mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-16 10:03:51 -07:00
New: Queued Task/Command List View
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
parent
9a1660da51
commit
60bb0ac063
31 changed files with 897 additions and 287 deletions
|
@ -30,7 +30,7 @@ import TagSettings from 'Settings/Tags/TagSettings';
|
||||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||||
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
|
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
|
||||||
import Status from 'System/Status/Status';
|
import Status from 'System/Status/Status';
|
||||||
import TasksConnector from 'System/Tasks/TasksConnector';
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
import BackupsConnector from 'System/Backup/BackupsConnector';
|
import BackupsConnector from 'System/Backup/BackupsConnector';
|
||||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||||
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||||
|
@ -218,7 +218,7 @@ function AppRoutes(props) {
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/system/tasks"
|
path="/system/tasks"
|
||||||
component={TasksConnector}
|
component={Tasks}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
|
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { findCommand } from 'Utilities/Command';
|
import { findCommand, isCommandExecuting } from 'Utilities/Command';
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
|
@ -43,13 +43,21 @@ function createMapStateToProps() {
|
||||||
|
|
||||||
const previousArtist = sortedArtist[artistIndex - 1] || _.last(sortedArtist);
|
const previousArtist = sortedArtist[artistIndex - 1] || _.last(sortedArtist);
|
||||||
const nextArtist = sortedArtist[artistIndex + 1] || _.first(sortedArtist);
|
const nextArtist = sortedArtist[artistIndex + 1] || _.first(sortedArtist);
|
||||||
const isArtistRefreshing = !!findCommand(commands, { name: commandNames.REFRESH_ARTIST, artistId: artist.id });
|
const isArtistRefreshing = isCommandExecuting(findCommand(commands, { name: commandNames.REFRESH_ARTIST, artistId: artist.id }));
|
||||||
const allArtistRefreshing = _.some(commands, (command) => command.name === commandNames.REFRESH_ARTIST && !command.body.artistId);
|
const artistRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_ARTIST });
|
||||||
|
const allArtistRefreshing = (
|
||||||
|
isCommandExecuting(artistRefreshingCommand) &&
|
||||||
|
!artistRefreshingCommand.body.artistId
|
||||||
|
);
|
||||||
const isRefreshing = isArtistRefreshing || allArtistRefreshing;
|
const isRefreshing = isArtistRefreshing || allArtistRefreshing;
|
||||||
const isSearching = !!findCommand(commands, { name: commandNames.ARTIST_SEARCH, artistId: artist.id });
|
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.ARTIST_SEARCH, artistId: artist.id }));
|
||||||
const isRenamingFiles = !!findCommand(commands, { name: commandNames.RENAME_FILES, artistId: artist.id });
|
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, artistId: artist.id }));
|
||||||
|
|
||||||
const isRenamingArtistCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST });
|
const isRenamingArtistCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST });
|
||||||
const isRenamingArtist = !!(isRenamingArtistCommand && isRenamingArtistCommand.body.artistId.indexOf(artist.id) > -1);
|
const isRenamingArtist = (
|
||||||
|
isCommandExecuting(isRenamingArtistCommand) &&
|
||||||
|
isRenamingArtistCommand.body.artistId.indexOf(artist.id) > -1
|
||||||
|
);
|
||||||
|
|
||||||
const isFetching = albums.isFetching || trackFiles.isFetching;
|
const isFetching = albums.isFetching || trackFiles.isFetching;
|
||||||
const isPopulated = albums.isPopulated && trackFiles.isPopulated;
|
const isPopulated = albums.isPopulated && trackFiles.isPopulated;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { findCommand } from 'Utilities/Command';
|
import { findCommand, isCommandExecuting } from 'Utilities/Command';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import createCommandSelector from 'Store/Selectors/createCommandSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import { setArtistEditorSort, setArtistEditorFilter, saveArtistEditor } from 'Store/Actions/artistEditorActions';
|
import { setArtistEditorSort, setArtistEditorFilter, saveArtistEditor } from 'Store/Actions/artistEditorActions';
|
||||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
@ -15,7 +15,7 @@ function createMapStateToProps() {
|
||||||
(state) => state.settings.languageProfiles,
|
(state) => state.settings.languageProfiles,
|
||||||
(state) => state.settings.metadataProfiles,
|
(state) => state.settings.metadataProfiles,
|
||||||
createClientSideCollectionSelector('artist', 'artistEditor'),
|
createClientSideCollectionSelector('artist', 'artistEditor'),
|
||||||
createCommandSelector(commandNames.RENAME_ARTIST),
|
createCommandExecutingSelector(commandNames.RENAME_ARTIST),
|
||||||
(languageProfiles, metadataProfiles, artist, isOrganizingArtist) => {
|
(languageProfiles, metadataProfiles, artist, isOrganizingArtist) => {
|
||||||
return {
|
return {
|
||||||
isOrganizingArtist,
|
isOrganizingArtist,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import createCommandSelector from 'Store/Selectors/createCommandSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import { fetchArtist } from 'Store/Actions/artistActions';
|
import { fetchArtist } from 'Store/Actions/artistActions';
|
||||||
import scrollPositions from 'Store/scrollPositions';
|
import scrollPositions from 'Store/scrollPositions';
|
||||||
|
@ -47,8 +47,8 @@ function getScrollTop(view, scrollTop, isSmallScreen) {
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createClientSideCollectionSelector('artist', 'artistIndex'),
|
createClientSideCollectionSelector('artist', 'artistIndex'),
|
||||||
createCommandSelector(commandNames.REFRESH_ARTIST),
|
createCommandExecutingSelector(commandNames.REFRESH_ARTIST),
|
||||||
createCommandSelector(commandNames.RSS_SYNC),
|
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
(
|
(
|
||||||
artist,
|
artist,
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { repopulatePage } from 'Utilities/pagePopulator';
|
import { repopulatePage } from 'Utilities/pagePopulator';
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
import { updateCommand, finishCommand } from 'Store/Actions/commandActions';
|
import { fetchCommands, updateCommand, finishCommand } from 'Store/Actions/commandActions';
|
||||||
import { setAppValue, setVersion } from 'Store/Actions/appActions';
|
import { setAppValue, setVersion } from 'Store/Actions/appActions';
|
||||||
import { update, updateItem, removeItem } from 'Store/Actions/baseActions';
|
import { update, updateItem, removeItem } from 'Store/Actions/baseActions';
|
||||||
import { fetchHealth } from 'Store/Actions/systemActions';
|
import { fetchHealth } from 'Store/Actions/systemActions';
|
||||||
|
@ -58,16 +58,17 @@ function createMapStateToProps() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
updateCommand,
|
dispatchFetchCommands: fetchCommands,
|
||||||
finishCommand,
|
dispatchUpdateCommand: updateCommand,
|
||||||
setAppValue,
|
dispatchFinishCommand: finishCommand,
|
||||||
setVersion,
|
dispatchSetAppValue: setAppValue,
|
||||||
update,
|
dispatchSetVersion: setVersion,
|
||||||
updateItem,
|
dispatchUpdate: update,
|
||||||
removeItem,
|
dispatchUpdateItem: updateItem,
|
||||||
fetchHealth,
|
dispatchRemoveItem: removeItem,
|
||||||
fetchQueue,
|
dispatchFetchHealth: fetchHealth,
|
||||||
fetchQueueDetails
|
dispatchFetchQueue: fetchQueue,
|
||||||
|
dispatchFetchQueueDetails: fetchQueueDetails
|
||||||
};
|
};
|
||||||
|
|
||||||
class SignalRConnector extends Component {
|
class SignalRConnector extends Component {
|
||||||
|
@ -146,7 +147,7 @@ class SignalRConnector extends Component {
|
||||||
|
|
||||||
handleCalendar = (body) => {
|
handleCalendar = (body) => {
|
||||||
if (body.action === 'updated') {
|
if (body.action === 'updated') {
|
||||||
this.props.updateItem({
|
this.props.dispatchUpdateItem({
|
||||||
section: 'calendar',
|
section: 'calendar',
|
||||||
updateOnly: true,
|
updateOnly: true,
|
||||||
...body.resource
|
...body.resource
|
||||||
|
@ -155,22 +156,27 @@ class SignalRConnector extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCommand = (body) => {
|
handleCommand = (body) => {
|
||||||
|
if (body.action === 'sync') {
|
||||||
|
this.props.dispatchFetchCommands();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const resource = body.resource;
|
const resource = body.resource;
|
||||||
const state = resource.state;
|
const status = resource.status;
|
||||||
|
|
||||||
// Both sucessful and failed commands need to be
|
// Both sucessful and failed commands need to be
|
||||||
// completed, otherwise they spin until they timeout.
|
// completed, otherwise they spin until they timeout.
|
||||||
|
|
||||||
if (state === 'completed' || state === 'failed') {
|
if (status === 'completed' || status === 'failed') {
|
||||||
this.props.finishCommand(resource);
|
this.props.dispatchFinishCommand(resource);
|
||||||
} else {
|
} else {
|
||||||
this.props.updateCommand(resource);
|
this.props.dispatchUpdateCommand(resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAlbum = (body) => {
|
handleAlbum = (body) => {
|
||||||
if (body.action === 'updated') {
|
if (body.action === 'updated') {
|
||||||
this.props.updateItem({
|
this.props.dispatchUpdateItem({
|
||||||
section: 'albums',
|
section: 'albums',
|
||||||
updateOnly: true,
|
updateOnly: true,
|
||||||
...body.resource
|
...body.resource
|
||||||
|
@ -180,7 +186,7 @@ class SignalRConnector extends Component {
|
||||||
|
|
||||||
handleTrack = (body) => {
|
handleTrack = (body) => {
|
||||||
if (body.action === 'updated') {
|
if (body.action === 'updated') {
|
||||||
this.props.updateItem({
|
this.props.dispatchUpdateItem({
|
||||||
section: 'tracks',
|
section: 'tracks',
|
||||||
updateOnly: true,
|
updateOnly: true,
|
||||||
...body.resource
|
...body.resource
|
||||||
|
@ -192,14 +198,14 @@ class SignalRConnector extends Component {
|
||||||
const section = 'trackFiles';
|
const section = 'trackFiles';
|
||||||
|
|
||||||
if (body.action === 'updated') {
|
if (body.action === 'updated') {
|
||||||
this.props.updateItem({ section, ...body.resource });
|
this.props.dispatchUpdateItem({ section, ...body.resource });
|
||||||
} else if (body.action === 'deleted') {
|
} else if (body.action === 'deleted') {
|
||||||
this.props.removeItem({ section, id: body.resource.id });
|
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHealth = () => {
|
handleHealth = () => {
|
||||||
this.props.fetchHealth();
|
this.props.dispatchFetchHealth();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleArtist = (body) => {
|
handleArtist = (body) => {
|
||||||
|
@ -207,35 +213,35 @@ class SignalRConnector extends Component {
|
||||||
const section = 'artist';
|
const section = 'artist';
|
||||||
|
|
||||||
if (action === 'updated') {
|
if (action === 'updated') {
|
||||||
this.props.updateItem({ section, ...body.resource });
|
this.props.dispatchUpdateItem({ section, ...body.resource });
|
||||||
} else if (action === 'deleted') {
|
} else if (action === 'deleted') {
|
||||||
this.props.removeItem({ section, id: body.resource.id });
|
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleQueue = () => {
|
handleQueue = () => {
|
||||||
if (this.props.isQueuePopulated) {
|
if (this.props.isQueuePopulated) {
|
||||||
this.props.fetchQueue();
|
this.props.dispatchFetchQueue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleQueueDetails = () => {
|
handleQueueDetails = () => {
|
||||||
this.props.fetchQueueDetails();
|
this.props.dispatchFetchQueueDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleQueueStatus = (body) => {
|
handleQueueStatus = (body) => {
|
||||||
this.props.update({ section: 'queue.status', data: body.resource });
|
this.props.dispatchUpdate({ section: 'queue.status', data: body.resource });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleVersion = (body) => {
|
handleVersion = (body) => {
|
||||||
const version = body.Version;
|
const version = body.Version;
|
||||||
|
|
||||||
this.props.setVersion({ version });
|
this.props.dispatchSetVersion({ version });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWantedCutoff = (body) => {
|
handleWantedCutoff = (body) => {
|
||||||
if (body.action === 'updated') {
|
if (body.action === 'updated') {
|
||||||
this.props.updateItem({
|
this.props.dispatchUpdateItem({
|
||||||
section: 'cutoffUnmet',
|
section: 'cutoffUnmet',
|
||||||
updateOnly: true,
|
updateOnly: true,
|
||||||
...body.resource
|
...body.resource
|
||||||
|
@ -245,7 +251,7 @@ class SignalRConnector extends Component {
|
||||||
|
|
||||||
handleWantedMissing = (body) => {
|
handleWantedMissing = (body) => {
|
||||||
if (body.action === 'updated') {
|
if (body.action === 'updated') {
|
||||||
this.props.updateItem({
|
this.props.dispatchUpdateItem({
|
||||||
section: 'missing',
|
section: 'missing',
|
||||||
updateOnly: true,
|
updateOnly: true,
|
||||||
...body.resource
|
...body.resource
|
||||||
|
@ -268,14 +274,20 @@ class SignalRConnector extends Component {
|
||||||
// Clear disconnected time
|
// Clear disconnected time
|
||||||
this.disconnectedTime = null;
|
this.disconnectedTime = null;
|
||||||
|
|
||||||
|
const {
|
||||||
|
dispatchFetchCommands,
|
||||||
|
dispatchSetAppValue
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
// Repopulate the page (if a repopulator is set) to ensure things
|
// Repopulate the page (if a repopulator is set) to ensure things
|
||||||
// are in sync after reconnecting.
|
// are in sync after reconnecting.
|
||||||
|
|
||||||
if (this.props.isReconnecting || this.props.isDisconnected) {
|
if (this.props.isReconnecting || this.props.isDisconnected) {
|
||||||
|
dispatchFetchCommands();
|
||||||
repopulatePage();
|
repopulatePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.setAppValue({
|
dispatchSetAppValue({
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
isReconnecting: false,
|
isReconnecting: false,
|
||||||
isDisconnected: false,
|
isDisconnected: false,
|
||||||
|
@ -305,7 +317,7 @@ class SignalRConnector extends Component {
|
||||||
this.disconnectedTime = Math.floor(new Date().getTime() / 1000);
|
this.disconnectedTime = Math.floor(new Date().getTime() / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.setAppValue({
|
this.props.dispatchSetAppValue({
|
||||||
isReconnecting: true
|
isReconnecting: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -319,7 +331,7 @@ class SignalRConnector extends Component {
|
||||||
this.disconnectedTime = Math.floor(new Date().getTime() / 1000);
|
this.disconnectedTime = Math.floor(new Date().getTime() / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.setAppValue({
|
this.props.dispatchSetAppValue({
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
isReconnecting: true,
|
isReconnecting: true,
|
||||||
isDisconnected: isAppDisconnected(this.disconnectedTime)
|
isDisconnected: isAppDisconnected(this.disconnectedTime)
|
||||||
|
@ -340,16 +352,17 @@ SignalRConnector.propTypes = {
|
||||||
isReconnecting: PropTypes.bool.isRequired,
|
isReconnecting: PropTypes.bool.isRequired,
|
||||||
isDisconnected: PropTypes.bool.isRequired,
|
isDisconnected: PropTypes.bool.isRequired,
|
||||||
isQueuePopulated: PropTypes.bool.isRequired,
|
isQueuePopulated: PropTypes.bool.isRequired,
|
||||||
updateCommand: PropTypes.func.isRequired,
|
dispatchFetchCommands: PropTypes.func.isRequired,
|
||||||
finishCommand: PropTypes.func.isRequired,
|
dispatchUpdateCommand: PropTypes.func.isRequired,
|
||||||
setAppValue: PropTypes.func.isRequired,
|
dispatchFinishCommand: PropTypes.func.isRequired,
|
||||||
setVersion: PropTypes.func.isRequired,
|
dispatchSetAppValue: PropTypes.func.isRequired,
|
||||||
update: PropTypes.func.isRequired,
|
dispatchSetVersion: PropTypes.func.isRequired,
|
||||||
updateItem: PropTypes.func.isRequired,
|
dispatchUpdate: PropTypes.func.isRequired,
|
||||||
removeItem: PropTypes.func.isRequired,
|
dispatchUpdateItem: PropTypes.func.isRequired,
|
||||||
fetchHealth: PropTypes.func.isRequired,
|
dispatchRemoveItem: PropTypes.func.isRequired,
|
||||||
fetchQueue: PropTypes.func.isRequired,
|
dispatchFetchHealth: PropTypes.func.isRequired,
|
||||||
fetchQueueDetails: PropTypes.func.isRequired
|
dispatchFetchQueue: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchQueueDetails: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(SignalRConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(SignalRConnector);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { messageTypes } from 'Helpers/Props';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import createFetchHandler from './Creators/createFetchHandler';
|
import createFetchHandler from './Creators/createFetchHandler';
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||||
import { showMessage, hideMessage } from './appActions';
|
import { showMessage, hideMessage } from './appActions';
|
||||||
import { updateItem } from './baseActions';
|
import { updateItem } from './baseActions';
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@ export const defaultState = {
|
||||||
|
|
||||||
export const FETCH_COMMANDS = 'commands/fetchCommands';
|
export const FETCH_COMMANDS = 'commands/fetchCommands';
|
||||||
export const EXECUTE_COMMAND = 'commands/executeCommand';
|
export const EXECUTE_COMMAND = 'commands/executeCommand';
|
||||||
|
export const CANCEL_COMMAND = 'commands/cancelCommand';
|
||||||
export const ADD_COMMAND = 'commands/updateCommand';
|
export const ADD_COMMAND = 'commands/updateCommand';
|
||||||
export const UPDATE_COMMAND = 'commands/finishCommand';
|
export const UPDATE_COMMAND = 'commands/finishCommand';
|
||||||
export const FINISH_COMMAND = 'commands/addCommand';
|
export const FINISH_COMMAND = 'commands/addCommand';
|
||||||
|
@ -45,6 +47,7 @@ export const REMOVE_COMMAND = 'commands/removeCommand';
|
||||||
|
|
||||||
export const fetchCommands = createThunk(FETCH_COMMANDS);
|
export const fetchCommands = createThunk(FETCH_COMMANDS);
|
||||||
export const executeCommand = createThunk(EXECUTE_COMMAND);
|
export const executeCommand = createThunk(EXECUTE_COMMAND);
|
||||||
|
export const cancelCommand = createThunk(CANCEL_COMMAND);
|
||||||
export const updateCommand = createThunk(UPDATE_COMMAND);
|
export const updateCommand = createThunk(UPDATE_COMMAND);
|
||||||
export const finishCommand = createThunk(FINISH_COMMAND);
|
export const finishCommand = createThunk(FINISH_COMMAND);
|
||||||
export const addCommand = createAction(ADD_COMMAND);
|
export const addCommand = createAction(ADD_COMMAND);
|
||||||
|
@ -60,7 +63,7 @@ function showCommandMessage(payload, dispatch) {
|
||||||
trigger,
|
trigger,
|
||||||
message,
|
message,
|
||||||
body = {},
|
body = {},
|
||||||
state
|
status
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -75,10 +78,10 @@ function showCommandMessage(payload, dispatch) {
|
||||||
let type = messageTypes.INFO;
|
let type = messageTypes.INFO;
|
||||||
let hideAfter = 0;
|
let hideAfter = 0;
|
||||||
|
|
||||||
if (state === 'completed') {
|
if (status === 'completed') {
|
||||||
type = messageTypes.SUCCESS;
|
type = messageTypes.SUCCESS;
|
||||||
hideAfter = 4;
|
hideAfter = 4;
|
||||||
} else if (state === 'failed') {
|
} else if (status === 'failed') {
|
||||||
type = messageTypes.ERROR;
|
type = messageTypes.ERROR;
|
||||||
hideAfter = trigger === 'manual' ? 10 : 4;
|
hideAfter = trigger === 'manual' ? 10 : 4;
|
||||||
}
|
}
|
||||||
|
@ -95,8 +98,7 @@ function showCommandMessage(payload, dispatch) {
|
||||||
function scheduleRemoveCommand(command, dispatch) {
|
function scheduleRemoveCommand(command, dispatch) {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
status,
|
status
|
||||||
body
|
|
||||||
} = command;
|
} = command;
|
||||||
|
|
||||||
if (status === 'queued') {
|
if (status === 'queued') {
|
||||||
|
@ -109,12 +111,6 @@ function scheduleRemoveCommand(command, dispatch) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5 minute timeout for executing disk access commands and
|
|
||||||
// 30 seconds for all other commands.
|
|
||||||
const timeout = body.requiresDiskAccess && status === 'started' ?
|
|
||||||
60000 * 5 :
|
|
||||||
30000;
|
|
||||||
|
|
||||||
removeCommandTimeoutIds[id] = setTimeout(() => {
|
removeCommandTimeoutIds[id] = setTimeout(() => {
|
||||||
dispatch(batchActions([
|
dispatch(batchActions([
|
||||||
removeCommand({ section: 'commands', id }),
|
removeCommand({ section: 'commands', id }),
|
||||||
|
@ -122,7 +118,7 @@ function scheduleRemoveCommand(command, dispatch) {
|
||||||
]));
|
]));
|
||||||
|
|
||||||
delete removeCommandTimeoutIds[id];
|
delete removeCommandTimeoutIds[id];
|
||||||
}, timeout);
|
}, 60000 * 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -159,6 +155,8 @@ export const actionHandlers = handleThunks({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[CANCEL_COMMAND]: createRemoveItemHandler(section, '/command'),
|
||||||
|
|
||||||
[UPDATE_COMMAND]: function(getState, payload, dispatch) {
|
[UPDATE_COMMAND]: function(getState, payload, dispatch) {
|
||||||
dispatch(updateItem({ section: 'commands', ...payload }));
|
dispatch(updateItem({ section: 'commands', ...payload }));
|
||||||
|
|
||||||
|
@ -178,7 +176,7 @@ export const actionHandlers = handleThunks({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch(removeCommand({ section: 'commands', ...payload }));
|
scheduleRemoveCommand(payload, dispatch);
|
||||||
showCommandMessage(payload, dispatch);
|
showCommandMessage(payload, dispatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import { findCommand, isCommandExecuting } from 'Utilities/Command';
|
||||||
import createCommandsSelector from './createCommandsSelector';
|
import createCommandsSelector from './createCommandsSelector';
|
||||||
|
|
||||||
function createCommandExecutingSelector(name) {
|
function createCommandExecutingSelector(name, contraints = {}) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createCommandsSelector(),
|
createCommandsSelector(),
|
||||||
(commands) => {
|
(commands) => {
|
||||||
return _.some(commands, { name });
|
return isCommandExecuting(findCommand(commands, { name, ...contraints }));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import { findCommand } from 'Utilities/Command';
|
||||||
import createCommandsSelector from './createCommandsSelector';
|
import createCommandsSelector from './createCommandsSelector';
|
||||||
|
|
||||||
function createCommandSelector(name, contraints = {}) {
|
function createCommandSelector(name, contraints = {}) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createCommandsSelector(),
|
createCommandsSelector(),
|
||||||
(commands) => {
|
(commands) => {
|
||||||
return _.some(commands, { name, ...contraints });
|
return !!findCommand(commands, { name, ...contraints });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
31
frontend/src/System/Tasks/Queued/QueuedTaskRow.css
Normal file
31
frontend/src/System/Tasks/Queued/QueuedTaskRow.css
Normal 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;
|
||||||
|
}
|
264
frontend/src/System/Tasks/Queued/QueuedTaskRow.js
Normal file
264
frontend/src/System/Tasks/Queued/QueuedTaskRow.js
Normal 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;
|
31
frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js
Normal file
31
frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js
Normal 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);
|
89
frontend/src/System/Tasks/Queued/QueuedTasks.js
Normal file
89
frontend/src/System/Tasks/Queued/QueuedTasks.js
Normal 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;
|
46
frontend/src/System/Tasks/Queued/QueuedTasksConnector.js
Normal file
46
frontend/src/System/Tasks/Queued/QueuedTasksConnector.js
Normal 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);
|
182
frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js
Normal file
182
frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js
Normal 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;
|
|
@ -2,12 +2,12 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { findCommand } from 'Utilities/Command';
|
import { findCommand, isCommandExecuting } from 'Utilities/Command';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import { fetchTask } from 'Store/Actions/systemActions';
|
import { fetchTask } from 'Store/Actions/systemActions';
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
import TaskRow from './TaskRow';
|
import ScheduledTaskRow from './ScheduledTaskRow';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
|
@ -15,10 +15,11 @@ function createMapStateToProps() {
|
||||||
createCommandsSelector(),
|
createCommandsSelector(),
|
||||||
createUISettingsSelector(),
|
createUISettingsSelector(),
|
||||||
(taskName, commands, uiSettings) => {
|
(taskName, commands, uiSettings) => {
|
||||||
const isExecuting = !!findCommand(commands, { name: taskName });
|
const command = findCommand(commands, { name: taskName });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isExecuting,
|
isQueued: !!(command && command.state === 'queued'),
|
||||||
|
isExecuting: isCommandExecuting(command),
|
||||||
showRelativeDates: uiSettings.showRelativeDates,
|
showRelativeDates: uiSettings.showRelativeDates,
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
shortDateFormat: uiSettings.shortDateFormat,
|
||||||
longDateFormat: uiSettings.longDateFormat,
|
longDateFormat: uiSettings.longDateFormat,
|
||||||
|
@ -46,7 +47,7 @@ function createMapDispatchToProps(dispatch, props) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class TaskRowConnector extends Component {
|
class ScheduledTaskRowConnector extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
@ -75,17 +76,17 @@ class TaskRowConnector extends Component {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TaskRow
|
<ScheduledTaskRow
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskRowConnector.propTypes = {
|
ScheduledTaskRowConnector.propTypes = {
|
||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.number.isRequired,
|
||||||
isExecuting: PropTypes.bool.isRequired,
|
isExecuting: PropTypes.bool.isRequired,
|
||||||
dispatchFetchTask: PropTypes.func.isRequired
|
dispatchFetchTask: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(TaskRowConnector);
|
export default connect(createMapStateToProps, createMapDispatchToProps)(ScheduledTaskRowConnector);
|
79
frontend/src/System/Tasks/Scheduled/ScheduledTasks.js
Normal file
79
frontend/src/System/Tasks/Scheduled/ScheduledTasks.js
Normal 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;
|
|
@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchTasks } from 'Store/Actions/systemActions';
|
import { fetchTasks } from 'Store/Actions/systemActions';
|
||||||
import Tasks from './Tasks';
|
import ScheduledTasks from './ScheduledTasks';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
|
@ -15,16 +15,16 @@ function createMapStateToProps() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
fetchTasks
|
dispatchFetchTasks: fetchTasks
|
||||||
};
|
};
|
||||||
|
|
||||||
class TasksConnector extends Component {
|
class ScheduledTasksConnector extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchTasks();
|
this.props.dispatchFetchTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -32,15 +32,15 @@ class TasksConnector extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Tasks
|
<ScheduledTasks
|
||||||
{...this.props}
|
{...this.props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TasksConnector.propTypes = {
|
ScheduledTasksConnector.propTypes = {
|
||||||
fetchTasks: PropTypes.func.isRequired
|
dispatchFetchTasks: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(TasksConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(ScheduledTasksConnector);
|
|
@ -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;
|
|
|
@ -1,89 +1,18 @@
|
||||||
import PropTypes from 'prop-types';
|
import React from 'react';
|
||||||
import React, { Component } from 'react';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
|
||||||
import Table from 'Components/Table/Table';
|
import QueuedTasksConnector from './Queued/QueuedTasksConnector';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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;
|
export default Tasks;
|
||||||
|
|
|
@ -3,7 +3,7 @@ function isCommandComplete(command) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return command.state === 'complete';
|
return command.status === 'complete';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default isCommandComplete;
|
export default isCommandComplete;
|
||||||
|
|
|
@ -3,7 +3,7 @@ function isCommandExecuting(command) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return command.state === 'queued' || command.state === 'started';
|
return command.status === 'queued' || command.status === 'started';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default isCommandExecuting;
|
export default isCommandExecuting;
|
||||||
|
|
|
@ -3,10 +3,10 @@ function isCommandFailed(command) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return command.state === 'failed' ||
|
return command.status === 'failed' ||
|
||||||
command.state === 'aborted' ||
|
command.status === 'aborted' ||
|
||||||
command.state === 'cancelled' ||
|
command.status === 'cancelled' ||
|
||||||
command.state === 'orphaned';
|
command.status === 'orphaned';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default isCommandFailed;
|
export default isCommandFailed;
|
||||||
|
|
|
@ -32,6 +32,7 @@ namespace Lidarr.Api.V1.Commands
|
||||||
GetResourceById = GetCommand;
|
GetResourceById = GetCommand;
|
||||||
CreateResource = StartCommand;
|
CreateResource = StartCommand;
|
||||||
GetResourceAll = GetStartedCommands;
|
GetResourceAll = GetStartedCommands;
|
||||||
|
DeleteResource = CancelCommand;
|
||||||
|
|
||||||
PostValidator.RuleFor(c => c.Name).NotBlank();
|
PostValidator.RuleFor(c => c.Name).NotBlank();
|
||||||
|
|
||||||
|
@ -62,7 +63,13 @@ namespace Lidarr.Api.V1.Commands
|
||||||
|
|
||||||
private List<CommandResource> GetStartedCommands()
|
private List<CommandResource> GetStartedCommands()
|
||||||
{
|
{
|
||||||
return _commandQueueManager.GetStarted().ToResource();
|
return _commandQueueManager.All().ToResource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelCommand(int id)
|
||||||
|
{
|
||||||
|
// TODO: Cancel the existing command
|
||||||
|
// Executing tasks should preferably exit gracefully
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Handle(CommandUpdatedEvent message)
|
public void Handle(CommandUpdatedEvent message)
|
||||||
|
@ -75,6 +82,13 @@ namespace Lidarr.Api.V1.Commands
|
||||||
}
|
}
|
||||||
_debouncer.Execute();
|
_debouncer.Execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.Command.Name == typeof(MessagingCleanupCommand).Name.Replace("Command", "") &&
|
||||||
|
message.Command.Status == CommandStatus.Completed)
|
||||||
|
{
|
||||||
|
BroadcastResourceChange(ModelAction.Sync);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SendUpdates()
|
private void SendUpdates()
|
||||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
using Lidarr.Http.REST;
|
using Lidarr.Http.REST;
|
||||||
|
|
||||||
|
@ -10,6 +11,7 @@ namespace Lidarr.Api.V1.Commands
|
||||||
public class CommandResource : RestResource
|
public class CommandResource : RestResource
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
public string CommandName { get; set; }
|
||||||
public string Message { get; set; }
|
public string Message { get; set; }
|
||||||
public Command Body { get; set; }
|
public Command Body { get; set; }
|
||||||
public CommandPriority Priority { get; set; }
|
public CommandPriority Priority { get; set; }
|
||||||
|
@ -75,6 +77,7 @@ namespace Lidarr.Api.V1.Commands
|
||||||
Id = model.Id,
|
Id = model.Id,
|
||||||
|
|
||||||
Name = model.Name,
|
Name = model.Name,
|
||||||
|
CommandName = model.Name.SplitCamelCase(),
|
||||||
Message = model.Message,
|
Message = model.Message,
|
||||||
Body = model.Body,
|
Body = model.Body,
|
||||||
Priority = model.Priority,
|
Priority = model.Priority,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Datastore.Events;
|
using NzbDrone.Core.Datastore.Events;
|
||||||
using NzbDrone.Core.Jobs;
|
using NzbDrone.Core.Jobs;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
@ -13,8 +14,6 @@ namespace Lidarr.Api.V1.System.Tasks
|
||||||
{
|
{
|
||||||
private readonly ITaskManager _taskManager;
|
private readonly ITaskManager _taskManager;
|
||||||
|
|
||||||
private static readonly Regex NameRegex = new Regex("(?<!^)[A-Z]", RegexOptions.Compiled);
|
|
||||||
|
|
||||||
public TaskModule(ITaskManager taskManager, IBroadcastSignalRMessage broadcastSignalRMessage)
|
public TaskModule(ITaskManager taskManager, IBroadcastSignalRMessage broadcastSignalRMessage)
|
||||||
: base(broadcastSignalRMessage, "system/task")
|
: base(broadcastSignalRMessage, "system/task")
|
||||||
{
|
{
|
||||||
|
@ -51,7 +50,7 @@ namespace Lidarr.Api.V1.System.Tasks
|
||||||
return new TaskResource
|
return new TaskResource
|
||||||
{
|
{
|
||||||
Id = scheduledTask.Id,
|
Id = scheduledTask.Id,
|
||||||
Name = NameRegex.Replace(taskName, match => " " + match.Value),
|
Name = taskName.SplitCamelCase(),
|
||||||
TaskName = taskName,
|
TaskName = taskName,
|
||||||
Interval = scheduledTask.Interval,
|
Interval = scheduledTask.Interval,
|
||||||
LastExecution = scheduledTask.LastExecution,
|
LastExecution = scheduledTask.LastExecution,
|
||||||
|
|
|
@ -46,17 +46,19 @@ namespace Lidarr.Http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected void BroadcastResourceChange(ModelAction action, TResource resource)
|
protected void BroadcastResourceChange(ModelAction action, TResource resource)
|
||||||
{
|
{
|
||||||
var signalRMessage = new SignalRMessage
|
if (GetType().Namespace.Contains("V1"))
|
||||||
{
|
{
|
||||||
Name = Resource,
|
var signalRMessage = new SignalRMessage
|
||||||
Body = new ResourceChangeMessage<TResource>(resource, action),
|
{
|
||||||
Action = action
|
Name = Resource,
|
||||||
};
|
Body = new ResourceChangeMessage<TResource>(resource, action),
|
||||||
|
Action = action
|
||||||
|
};
|
||||||
|
|
||||||
_signalRBroadcaster.BroadcastMessage(signalRMessage);
|
_signalRBroadcaster.BroadcastMessage(signalRMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ namespace NzbDrone.Common.Extensions
|
||||||
{
|
{
|
||||||
public static class StringExtensions
|
public static class StringExtensions
|
||||||
{
|
{
|
||||||
|
private static readonly Regex CamelCaseRegex = new Regex("(?<!^)[A-Z]", RegexOptions.Compiled);
|
||||||
|
|
||||||
public static string NullSafe(this string target)
|
public static string NullSafe(this string target)
|
||||||
{
|
{
|
||||||
return ((object)target).NullSafe().ToString();
|
return ((object)target).NullSafe().ToString();
|
||||||
|
@ -133,5 +135,11 @@ namespace NzbDrone.Common.Extensions
|
||||||
|
|
||||||
return Encoding.ASCII.GetString(new [] { byteResult });
|
return Encoding.ASCII.GetString(new [] { byteResult });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string SplitCamelCase(this string input)
|
||||||
|
{
|
||||||
|
return CamelCaseRegex.Replace(input, match => " " + match.Value);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,7 +151,7 @@ namespace NzbDrone.Core.Messaging.Commands
|
||||||
// A command ready to execute
|
// A command ready to execute
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
localItem.StartedAt = DateTime.Now;
|
localItem.StartedAt = DateTime.UtcNow;
|
||||||
localItem.Status = CommandStatus.Started;
|
localItem.Status = CommandStatus.Started;
|
||||||
|
|
||||||
item = localItem;
|
item = localItem;
|
||||||
|
|
|
@ -17,6 +17,7 @@ namespace NzbDrone.Core.Messaging.Commands
|
||||||
CommandModel Push<TCommand>(TCommand command, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified) where TCommand : Command;
|
CommandModel Push<TCommand>(TCommand command, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified) where TCommand : Command;
|
||||||
CommandModel Push(string commandName, DateTime? lastExecutionTime, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified);
|
CommandModel Push(string commandName, DateTime? lastExecutionTime, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified);
|
||||||
IEnumerable<CommandModel> Queue(CancellationToken cancellationToken);
|
IEnumerable<CommandModel> Queue(CancellationToken cancellationToken);
|
||||||
|
List<CommandModel> All();
|
||||||
CommandModel Get(int id);
|
CommandModel Get(int id);
|
||||||
List<CommandModel> GetStarted();
|
List<CommandModel> GetStarted();
|
||||||
void SetMessage(CommandModel command, string message);
|
void SetMessage(CommandModel command, string message);
|
||||||
|
@ -136,6 +137,12 @@ namespace NzbDrone.Core.Messaging.Commands
|
||||||
return _commandQueue.GetConsumingEnumerable(cancellationToken);
|
return _commandQueue.GetConsumingEnumerable(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<CommandModel> All()
|
||||||
|
{
|
||||||
|
_logger.Trace("Getting all commands");
|
||||||
|
return _commandQueue.All();
|
||||||
|
}
|
||||||
|
|
||||||
public CommandModel Get(int id)
|
public CommandModel Get(int id)
|
||||||
{
|
{
|
||||||
var command = _commandQueue.Find(id);
|
var command = _commandQueue.Find(id);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue