mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Project Aphrodite
This commit is contained in:
12
frontend/src/System/Backup/BackupRow.css
Normal file
12
frontend/src/System/Backup/BackupRow.css
Normal file
@@ -0,0 +1,12 @@
|
||||
.type {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 70px;
|
||||
}
|
153
frontend/src/System/Backup/BackupRow.js
Normal file
153
frontend/src/System/Backup/BackupRow.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import RestoreBackupModalConnector from './RestoreBackupModalConnector';
|
||||
import styles from './BackupRow.css';
|
||||
|
||||
class BackupRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isRestoreModalOpen: false,
|
||||
isConfirmDeleteModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRestorePress = () => {
|
||||
this.setState({ isRestoreModalOpen: true });
|
||||
}
|
||||
|
||||
onRestoreModalClose = () => {
|
||||
this.setState({ isRestoreModalOpen: false });
|
||||
}
|
||||
|
||||
onDeletePress = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: true });
|
||||
}
|
||||
|
||||
onConfirmDeleteModalClose = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmDeletePress = () => {
|
||||
const {
|
||||
id,
|
||||
onDeleteBackupPress
|
||||
} = this.props;
|
||||
|
||||
this.setState({ isConfirmDeleteModalOpen: false }, () => {
|
||||
onDeleteBackupPress(id);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
path,
|
||||
time
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isRestoreModalOpen,
|
||||
isConfirmDeleteModalOpen
|
||||
} = this.state;
|
||||
|
||||
let iconClassName = icons.SCHEDULED;
|
||||
let iconTooltip = 'Scheduled';
|
||||
|
||||
if (type === 'manual') {
|
||||
iconClassName = icons.INTERACTIVE;
|
||||
iconTooltip = 'Manual';
|
||||
} else if (type === 'update') {
|
||||
iconClassName = icons.UPDATE;
|
||||
iconTooltip = 'Before update';
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow key={id}>
|
||||
<TableRowCell className={styles.type}>
|
||||
{
|
||||
<Icon
|
||||
name={iconClassName}
|
||||
title={iconTooltip}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<Link
|
||||
to={path}
|
||||
noRouter={true}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</TableRowCell>
|
||||
|
||||
<RelativeDateCellConnector
|
||||
date={time}
|
||||
/>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
name={icons.RESTORE}
|
||||
onPress={this.onRestorePress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
title="Delete backup"
|
||||
name={icons.DELETE}
|
||||
onPress={this.onDeletePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<RestoreBackupModalConnector
|
||||
isOpen={isRestoreModalOpen}
|
||||
id={id}
|
||||
name={name}
|
||||
onModalClose={this.onRestoreModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Delete Backup"
|
||||
message={`Are you sure you want to delete the backup '${name}'?`}
|
||||
confirmLabel="Delete"
|
||||
onConfirm={this.onConfirmDeletePress}
|
||||
onCancel={this.onConfirmDeleteModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BackupRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
time: PropTypes.string.isRequired,
|
||||
onDeleteBackupPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BackupRow;
|
166
frontend/src/System/Backup/Backups.js
Normal file
166
frontend/src/System/Backup/Backups.js
Normal file
@@ -0,0 +1,166 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import BackupRow from './BackupRow';
|
||||
import RestoreBackupModalConnector from './RestoreBackupModalConnector';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'type',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'time',
|
||||
label: 'Time',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class Backups extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isRestoreModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRestorePress = () => {
|
||||
this.setState({ isRestoreModalOpen: true });
|
||||
}
|
||||
|
||||
onRestoreModalClose = () => {
|
||||
this.setState({ isRestoreModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
backupExecuting,
|
||||
onBackupPress,
|
||||
onDeleteBackupPress
|
||||
} = this.props;
|
||||
|
||||
const hasBackups = isPopulated && !!items.length;
|
||||
const noBackups = isPopulated && !items.length;
|
||||
|
||||
return (
|
||||
<PageContent title="Backups">
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Backup Now"
|
||||
iconName={icons.BACKUP}
|
||||
isSpinning={backupExecuting}
|
||||
onPress={onBackupPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Restore Backup"
|
||||
iconName={icons.RESTORE}
|
||||
onPress={this.onRestorePress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to load backups</div>
|
||||
}
|
||||
|
||||
{
|
||||
noBackups &&
|
||||
<div>No backups are available</div>
|
||||
}
|
||||
|
||||
{
|
||||
hasBackups &&
|
||||
<Table
|
||||
columns={columns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
path,
|
||||
time
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<BackupRow
|
||||
key={id}
|
||||
id={id}
|
||||
type={type}
|
||||
name={name}
|
||||
path={path}
|
||||
time={time}
|
||||
onDeleteBackupPress={onDeleteBackupPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
|
||||
<RestoreBackupModalConnector
|
||||
isOpen={this.state.isRestoreModalOpen}
|
||||
onModalClose={this.onRestoreModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Backups.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.array.isRequired,
|
||||
backupExecuting: PropTypes.bool.isRequired,
|
||||
onBackupPress: PropTypes.func.isRequired,
|
||||
onDeleteBackupPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Backups;
|
84
frontend/src/System/Backup/BackupsConnector.js
Normal file
84
frontend/src/System/Backup/BackupsConnector.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { fetchBackups, deleteBackup } from 'Store/Actions/systemActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Backups from './Backups';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.backups,
|
||||
createCommandExecutingSelector(commandNames.BACKUP),
|
||||
(backups, backupExecuting) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = backups;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
backupExecuting
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchFetchBackups() {
|
||||
dispatch(fetchBackups());
|
||||
},
|
||||
|
||||
onDeleteBackupPress(id) {
|
||||
dispatch(deleteBackup({ id }));
|
||||
},
|
||||
|
||||
onBackupPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.BACKUP
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class BackupsConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchBackups();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.backupExecuting && !this.props.backupExecuting) {
|
||||
this.props.dispatchFetchBackups();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Backups
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BackupsConnector.propTypes = {
|
||||
backupExecuting: PropTypes.bool.isRequired,
|
||||
dispatchFetchBackups: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(BackupsConnector);
|
31
frontend/src/System/Backup/RestoreBackupModal.js
Normal file
31
frontend/src/System/Backup/RestoreBackupModal.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import RestoreBackupModalContentConnector from './RestoreBackupModalContentConnector';
|
||||
|
||||
function RestoreBackupModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<RestoreBackupModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
RestoreBackupModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RestoreBackupModal;
|
15
frontend/src/System/Backup/RestoreBackupModalConnector.js
Normal file
15
frontend/src/System/Backup/RestoreBackupModalConnector.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { clearRestoreBackup } from 'Store/Actions/systemActions';
|
||||
import RestoreBackupModal from './RestoreBackupModal';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onModalClose() {
|
||||
dispatch(clearRestoreBackup());
|
||||
|
||||
props.onModalClose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, createMapDispatchToProps)(RestoreBackupModal);
|
24
frontend/src/System/Backup/RestoreBackupModalContent.css
Normal file
24
frontend/src/System/Backup/RestoreBackupModalContent.css
Normal file
@@ -0,0 +1,24 @@
|
||||
.additionalInfo {
|
||||
flex-grow: 1;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.steps {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
font-size: $largeFontSize;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.stepState {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
composes: modalFooter from 'Components/Modal/ModalFooter.css';
|
||||
|
||||
flex-wrap: wrap;
|
||||
}
|
232
frontend/src/System/Backup/RestoreBackupModalContent.js
Normal file
232
frontend/src/System/Backup/RestoreBackupModalContent.js
Normal file
@@ -0,0 +1,232 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './RestoreBackupModalContent.css';
|
||||
|
||||
function getErrorMessage(error) {
|
||||
if (!error || !error.responseJSON || !error.responseJSON.message) {
|
||||
return 'Error restoring backup';
|
||||
}
|
||||
|
||||
return error.responseJSON.message;
|
||||
}
|
||||
|
||||
function getStepIconProps(isExecuting, hasExecuted, error) {
|
||||
if (isExecuting) {
|
||||
return {
|
||||
name: icons.SPINNER,
|
||||
isSpinning: true
|
||||
};
|
||||
}
|
||||
|
||||
if (hasExecuted) {
|
||||
return {
|
||||
name: icons.CHECK,
|
||||
kind: kinds.SUCCESS
|
||||
};
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
name: icons.FATAL,
|
||||
kinds: kinds.DANGER,
|
||||
title: getErrorMessage(error)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: icons.PENDING
|
||||
};
|
||||
}
|
||||
|
||||
class RestoreBackupModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
file: null,
|
||||
path: '',
|
||||
isRestored: false,
|
||||
isRestarted: false,
|
||||
isReloading: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isRestoring,
|
||||
restoreError,
|
||||
isRestarting,
|
||||
dispatchRestart
|
||||
} = this.props;
|
||||
|
||||
if (prevProps.isRestoring && !isRestoring && !restoreError) {
|
||||
this.setState({ isRestored: true }, () => {
|
||||
dispatchRestart();
|
||||
});
|
||||
}
|
||||
|
||||
if (prevProps.isRestarting && !isRestarting) {
|
||||
this.setState({
|
||||
isRestarted: true,
|
||||
isReloading: true
|
||||
}, () => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPathChange = ({ value, files }) => {
|
||||
this.setState({
|
||||
file: files[0],
|
||||
path: value
|
||||
});
|
||||
}
|
||||
|
||||
onRestorePress = () => {
|
||||
const {
|
||||
id,
|
||||
onRestorePress
|
||||
} = this.props;
|
||||
|
||||
onRestorePress({
|
||||
id,
|
||||
file: this.state.file
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
isRestoring,
|
||||
restoreError,
|
||||
isRestarting,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
path,
|
||||
isRestored,
|
||||
isRestarted,
|
||||
isReloading
|
||||
} = this.state;
|
||||
|
||||
const isRestoreDisabled = (
|
||||
(!id && !path) ||
|
||||
isRestoring ||
|
||||
isRestarting ||
|
||||
isReloading
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Restore Backup
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
!!id && `Would you like to restore the backup '${name}'?`
|
||||
}
|
||||
|
||||
{
|
||||
!id &&
|
||||
<TextInput
|
||||
type="file"
|
||||
name="path"
|
||||
value={path}
|
||||
onChange={this.onPathChange}
|
||||
/>
|
||||
}
|
||||
|
||||
<div className={styles.steps}>
|
||||
<div className={styles.step}>
|
||||
<div className={styles.stepState}>
|
||||
<Icon
|
||||
size={20}
|
||||
{...getStepIconProps(isRestoring, isRestored, restoreError)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>Restore</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.step}>
|
||||
<div className={styles.stepState}>
|
||||
<Icon
|
||||
size={20}
|
||||
{...getStepIconProps(isRestarting, isRestarted)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>Restart</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.step}>
|
||||
<div className={styles.stepState}>
|
||||
<Icon
|
||||
size={20}
|
||||
{...getStepIconProps(isReloading, false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>Reload</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<div className={styles.additionalInfo}>
|
||||
Note: Radarr will automatically restart and reload the UI during the restore process.
|
||||
</div>
|
||||
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<SpinnerButton
|
||||
kind={kinds.WARNING}
|
||||
isDisabled={isRestoreDisabled}
|
||||
isSpinning={isRestoring}
|
||||
onPress={this.onRestorePress}
|
||||
>
|
||||
Restore
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RestoreBackupModalContent.propTypes = {
|
||||
id: PropTypes.number,
|
||||
name: PropTypes.string,
|
||||
path: PropTypes.string,
|
||||
isRestoring: PropTypes.bool.isRequired,
|
||||
restoreError: PropTypes.object,
|
||||
isRestarting: PropTypes.bool.isRequired,
|
||||
dispatchRestart: PropTypes.func.isRequired,
|
||||
onRestorePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RestoreBackupModalContent;
|
@@ -0,0 +1,37 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { restoreBackup, restart } from 'Store/Actions/systemActions';
|
||||
import RestoreBackupModalContent from './RestoreBackupModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.backups,
|
||||
(state) => state.app.isRestarting,
|
||||
(backups, isRestarting) => {
|
||||
const {
|
||||
isRestoring,
|
||||
restoreError
|
||||
} = backups;
|
||||
|
||||
return {
|
||||
isRestoring,
|
||||
restoreError,
|
||||
isRestarting
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRestorePress(payload) {
|
||||
dispatch(restoreBackup(payload));
|
||||
},
|
||||
|
||||
dispatchRestart() {
|
||||
dispatch(restart());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(RestoreBackupModalContent);
|
139
frontend/src/System/Events/LogsTable.js
Normal file
139
frontend/src/System/Events/LogsTable.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import LogsTableRow from './LogsTableRow';
|
||||
|
||||
function LogsTable(props) {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
totalRecords,
|
||||
clearLogExecuting,
|
||||
onRefreshPress,
|
||||
onClearLogsPress,
|
||||
onFilterSelect,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<PageContent title="Logs">
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Refresh"
|
||||
iconName={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
isSpinning={isFetching}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Clear"
|
||||
iconName={icons.CLEAR}
|
||||
isSpinning={clearLogExecuting}
|
||||
onPress={onClearLogsPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
canModifyColumns={false}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={[]}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<div>
|
||||
No logs found
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
canModifyColumns={false}
|
||||
{...otherProps}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<LogsTableRow
|
||||
key={item.id}
|
||||
columns={columns}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
LogsTable.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
clearLogExecuting: PropTypes.bool.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onClearLogsPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default LogsTable;
|
141
frontend/src/System/Events/LogsTableConnector.js
Normal file
141
frontend/src/System/Events/LogsTableConnector.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as systemActions from 'Store/Actions/systemActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import LogsTable from './LogsTable';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.logs,
|
||||
createCommandExecutingSelector(commandNames.CLEAR_LOGS),
|
||||
(logs, clearLogExecuting) => {
|
||||
return {
|
||||
clearLogExecuting,
|
||||
...logs
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
executeCommand,
|
||||
...systemActions
|
||||
};
|
||||
|
||||
class LogsTableConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchLogs,
|
||||
gotoLogsFirstPage
|
||||
} = this.props;
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchLogs();
|
||||
} else {
|
||||
gotoLogsFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.clearLogExecuting && !this.props.clearLogExecuting) {
|
||||
this.props.gotoLogsFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoLogsFirstPage();
|
||||
}
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoLogsPreviousPage();
|
||||
}
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoLogsNextPage();
|
||||
}
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoLogsLastPage();
|
||||
}
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoLogsPage({ page });
|
||||
}
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setLogsSort({ sortKey });
|
||||
}
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setLogsFilter({ selectedFilterKey });
|
||||
}
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setLogsTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoLogsFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.gotoLogsFirstPage();
|
||||
}
|
||||
|
||||
onClearLogsPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_LOGS });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<LogsTable
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
onClearLogsPress={this.onClearLogsPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LogsTableConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
clearLogExecuting: PropTypes.bool.isRequired,
|
||||
fetchLogs: PropTypes.func.isRequired,
|
||||
gotoLogsFirstPage: PropTypes.func.isRequired,
|
||||
gotoLogsPreviousPage: PropTypes.func.isRequired,
|
||||
gotoLogsNextPage: PropTypes.func.isRequired,
|
||||
gotoLogsLastPage: PropTypes.func.isRequired,
|
||||
gotoLogsPage: PropTypes.func.isRequired,
|
||||
setLogsSort: PropTypes.func.isRequired,
|
||||
setLogsFilter: PropTypes.func.isRequired,
|
||||
setLogsTableOption: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(LogsTableConnector)
|
||||
);
|
17
frontend/src/System/Events/LogsTableDetailsModal.css
Normal file
17
frontend/src/System/Events/LogsTableDetailsModal.css
Normal file
@@ -0,0 +1,17 @@
|
||||
.detailsText {
|
||||
composes: scroller from 'Components/Scroller/Scroller.css';
|
||||
|
||||
display: block;
|
||||
margin: 0 0 10.5px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: #f5f5f5;
|
||||
color: #3a3f51;
|
||||
white-space: pre;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-size: 13px;
|
||||
font-family: $monoSpaceFontFamily;
|
||||
line-height: 1.52857143;
|
||||
}
|
74
frontend/src/System/Events/LogsTableDetailsModal.js
Normal file
74
frontend/src/System/Events/LogsTableDetailsModal.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './LogsTableDetailsModal.css';
|
||||
|
||||
function LogsTableDetailsModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
message,
|
||||
exception,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Details
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>Message</div>
|
||||
|
||||
<Scroller
|
||||
className={styles.detailsText}
|
||||
scrollDirection={scrollDirections.HORIZONTAL}
|
||||
>
|
||||
{message}
|
||||
</Scroller>
|
||||
|
||||
{
|
||||
!!exception &&
|
||||
<div>
|
||||
<div>Exception</div>
|
||||
<Scroller
|
||||
className={styles.detailsText}
|
||||
scrollDirection={scrollDirections.HORIZONTAL}
|
||||
>
|
||||
{exception}
|
||||
</Scroller>
|
||||
</div>
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
LogsTableDetailsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
exception: PropTypes.string,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default LogsTableDetailsModal;
|
35
frontend/src/System/Events/LogsTableRow.css
Normal file
35
frontend/src/System/Events/LogsTableRow.css
Normal file
@@ -0,0 +1,35 @@
|
||||
.level {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.info {
|
||||
color: #1e90ff;
|
||||
}
|
||||
|
||||
.debug {
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.trace {
|
||||
color: #d3d3d3;
|
||||
}
|
||||
|
||||
.warn {
|
||||
color: $warningColor;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $dangerColor;
|
||||
}
|
||||
|
||||
.fatal {
|
||||
color: $purple;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 45px;
|
||||
}
|
158
frontend/src/System/Events/LogsTableRow.js
Normal file
158
frontend/src/System/Events/LogsTableRow.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import LogsTableDetailsModal from './LogsTableDetailsModal';
|
||||
import styles from './LogsTableRow.css';
|
||||
|
||||
function getIconName(level) {
|
||||
switch (level) {
|
||||
case 'trace':
|
||||
case 'debug':
|
||||
case 'info':
|
||||
return icons.INFO;
|
||||
case 'warn':
|
||||
return icons.DANGER;
|
||||
case 'error':
|
||||
return icons.BUG;
|
||||
case 'fatal':
|
||||
return icons.FATAL;
|
||||
default:
|
||||
return icons.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
class LogsTableRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isDetailsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
// Don't re-open the modal if it's already open
|
||||
if (!this.state.isDetailsModalOpen) {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
}
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
level,
|
||||
logger,
|
||||
message,
|
||||
time,
|
||||
exception,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRowButton
|
||||
overlayContent={true}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'level') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.level}
|
||||
>
|
||||
<Icon
|
||||
className={styles[level]}
|
||||
name={getIconName(level)}
|
||||
title={level}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'logger') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{logger}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'message') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{message}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'time') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={time}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.actions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
<LogsTableDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
message={message}
|
||||
exception={exception}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
</TableRowButton>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
LogsTableRow.propTypes = {
|
||||
level: PropTypes.string.isRequired,
|
||||
logger: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
time: PropTypes.string.isRequired,
|
||||
exception: PropTypes.string,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default LogsTableRow;
|
139
frontend/src/System/Logs/Files/LogFiles.js
Normal file
139
frontend/src/System/Logs/Files/LogFiles.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Alert from 'Components/Alert';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import LogsNavMenu from '../LogsNavMenu';
|
||||
import LogFilesTableRow from './LogFilesTableRow';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'filename',
|
||||
label: 'Filename',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'lastWriteTime',
|
||||
label: 'Last Write Time',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'download',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class LogFiles extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
items,
|
||||
deleteFilesExecuting,
|
||||
currentLogView,
|
||||
location,
|
||||
onRefreshPress,
|
||||
onDeleteFilesPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<PageContent title="Log Files">
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<LogsNavMenu current={currentLogView} />
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="Refresh"
|
||||
iconName={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
isSpinning={isFetching}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Clear"
|
||||
iconName={icons.CLEAR}
|
||||
isSpinning={deleteFilesExecuting}
|
||||
onPress={onDeleteFilesPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
<PageContentBodyConnector>
|
||||
<Alert>
|
||||
<div>
|
||||
Log files are located in: {location}
|
||||
</div>
|
||||
|
||||
{
|
||||
currentLogView === 'Log Files' &&
|
||||
<div>
|
||||
The log level defaults to 'Info' and can be changed in <Link to="/settings/general">General Settings</Link>
|
||||
</div>
|
||||
}
|
||||
</Alert>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<LogFilesTableRow
|
||||
key={item.id}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !items.length &&
|
||||
<div>No log files</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
LogFiles.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
items: PropTypes.array.isRequired,
|
||||
deleteFilesExecuting: PropTypes.bool.isRequired,
|
||||
currentLogView: PropTypes.string.isRequired,
|
||||
location: PropTypes.string.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onDeleteFilesPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default LogFiles;
|
90
frontend/src/System/Logs/Files/LogFilesConnector.js
Normal file
90
frontend/src/System/Logs/Files/LogFilesConnector.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import combinePath from 'Utilities/String/combinePath';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchLogFiles } from 'Store/Actions/systemActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import LogFiles from './LogFiles';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.logFiles,
|
||||
(state) => state.system.status.item,
|
||||
createCommandExecutingSelector(commandNames.DELETE_LOG_FILES),
|
||||
(logFiles, status, deleteFilesExecuting) => {
|
||||
const {
|
||||
isFetching,
|
||||
items
|
||||
} = logFiles;
|
||||
|
||||
const {
|
||||
appData,
|
||||
isWindows
|
||||
} = status;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
items,
|
||||
deleteFilesExecuting,
|
||||
currentLogView: 'Log Files',
|
||||
location: combinePath(isWindows, appData, ['logs'])
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchLogFiles,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class LogFilesConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchLogFiles();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.deleteFilesExecuting && !this.props.deleteFilesExecuting) {
|
||||
this.props.fetchLogFiles();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.fetchLogFiles();
|
||||
}
|
||||
|
||||
onDeleteFilesPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.DELETE_LOG_FILES });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<LogFiles
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
onDeleteFilesPress={this.onDeleteFilesPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LogFilesConnector.propTypes = {
|
||||
deleteFilesExecuting: PropTypes.bool.isRequired,
|
||||
fetchLogFiles: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(LogFilesConnector);
|
5
frontend/src/System/Logs/Files/LogFilesTableRow.css
Normal file
5
frontend/src/System/Logs/Files/LogFilesTableRow.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.download {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 100px;
|
||||
}
|
50
frontend/src/System/Logs/Files/LogFilesTableRow.js
Normal file
50
frontend/src/System/Logs/Files/LogFilesTableRow.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import styles from './LogFilesTableRow.css';
|
||||
|
||||
class LogFilesTableRow extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
filename,
|
||||
lastWriteTime,
|
||||
downloadUrl
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell>{filename}</TableRowCell>
|
||||
|
||||
<RelativeDateCellConnector
|
||||
date={lastWriteTime}
|
||||
/>
|
||||
|
||||
<TableRowCell className={styles.download}>
|
||||
<Link
|
||||
to={downloadUrl}
|
||||
target="_blank"
|
||||
noRouter={true}
|
||||
>
|
||||
Download
|
||||
</Link>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
LogFilesTableRow.propTypes = {
|
||||
filename: PropTypes.string.isRequired,
|
||||
lastWriteTime: PropTypes.string.isRequired,
|
||||
downloadUrl: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default LogFilesTableRow;
|
30
frontend/src/System/Logs/Logs.js
Normal file
30
frontend/src/System/Logs/Logs.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import LogFilesConnector from './Files/LogFilesConnector';
|
||||
import UpdateLogFilesConnector from './Updates/UpdateLogFilesConnector';
|
||||
|
||||
class Logs extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
exact={true}
|
||||
path="/system/logs/files"
|
||||
component={LogFilesConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/system/logs/files/update"
|
||||
component={UpdateLogFilesConnector}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Logs;
|
71
frontend/src/System/Logs/LogsNavMenu.js
Normal file
71
frontend/src/System/Logs/LogsNavMenu.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
|
||||
class LogsNavMenu extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isMenuOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMenuButtonPress = () => {
|
||||
this.setState({ isMenuOpen: !this.state.isMenuOpen });
|
||||
}
|
||||
|
||||
onMenuItemPress = () => {
|
||||
this.setState({ isMenuOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
current
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
onPress={this.onMenuButtonPress}
|
||||
>
|
||||
{current}
|
||||
</MenuButton>
|
||||
<MenuContent
|
||||
isOpen={this.state.isMenuOpen}
|
||||
>
|
||||
<MenuItem
|
||||
to={'/system/logs/files'}
|
||||
>
|
||||
Log Files
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
to={'/system/logs/files/update'}
|
||||
>
|
||||
Updater Log Files
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LogsNavMenu.propTypes = {
|
||||
current: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default LogsNavMenu;
|
90
frontend/src/System/Logs/Updates/UpdateLogFilesConnector.js
Normal file
90
frontend/src/System/Logs/Updates/UpdateLogFilesConnector.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import combinePath from 'Utilities/String/combinePath';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchUpdateLogFiles } from 'Store/Actions/systemActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import LogFiles from '../Files/LogFiles';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.updateLogFiles,
|
||||
(state) => state.system.status.item,
|
||||
createCommandExecutingSelector(commandNames.DELETE_UPDATE_LOG_FILES),
|
||||
(updateLogFiles, status, deleteFilesExecuting) => {
|
||||
const {
|
||||
isFetching,
|
||||
items
|
||||
} = updateLogFiles;
|
||||
|
||||
const {
|
||||
appData,
|
||||
isWindows
|
||||
} = status;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
items,
|
||||
deleteFilesExecuting,
|
||||
currentLogView: 'Updater Log Files',
|
||||
location: combinePath(isWindows, appData, ['UpdateLogs'])
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchUpdateLogFiles,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class UpdateLogFilesConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchUpdateLogFiles();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.deleteFilesExecuting && !this.props.deleteFilesExecuting) {
|
||||
this.props.fetchUpdateLogFiles();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.fetchUpdateLogFiles();
|
||||
}
|
||||
|
||||
onDeleteFilesPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.DELETE_UPDATE_LOG_FILES });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<LogFiles
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
onDeleteFilesPress={this.onDeleteFilesPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateLogFilesConnector.propTypes = {
|
||||
deleteFilesExecuting: PropTypes.bool.isRequired,
|
||||
fetchUpdateLogFiles: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(UpdateLogFilesConnector);
|
5
frontend/src/System/Status/About/About.css
Normal file
5
frontend/src/System/Status/About/About.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.descriptionList {
|
||||
composes: descriptionList from 'Components/DescriptionList/DescriptionList.css';
|
||||
|
||||
margin-bottom: 10px;
|
||||
}
|
88
frontend/src/System/Status/About/About.js
Normal file
88
frontend/src/System/Status/About/About.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import StartTime from './StartTime';
|
||||
import styles from './About.css';
|
||||
|
||||
class About extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
version,
|
||||
isMonoRuntime,
|
||||
runtimeVersion,
|
||||
appData,
|
||||
startupPath,
|
||||
mode,
|
||||
startTime,
|
||||
timeFormat,
|
||||
longDateFormat
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FieldSet legend="About">
|
||||
<DescriptionList className={styles.descriptionList}>
|
||||
<DescriptionListItem
|
||||
title="Version"
|
||||
data={version}
|
||||
/>
|
||||
|
||||
{
|
||||
isMonoRuntime &&
|
||||
<DescriptionListItem
|
||||
title="Mono Version"
|
||||
data={runtimeVersion}
|
||||
/>
|
||||
}
|
||||
|
||||
<DescriptionListItem
|
||||
title="AppData directory"
|
||||
data={appData}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Startup directory"
|
||||
data={startupPath}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Mode"
|
||||
data={titleCase(mode)}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Uptime"
|
||||
data={
|
||||
<StartTime
|
||||
startTime={startTime}
|
||||
timeFormat={timeFormat}
|
||||
longDateFormat={longDateFormat}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</DescriptionList>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
About.propTypes = {
|
||||
version: PropTypes.string.isRequired,
|
||||
isMonoRuntime: PropTypes.bool.isRequired,
|
||||
runtimeVersion: PropTypes.string.isRequired,
|
||||
appData: PropTypes.string.isRequired,
|
||||
startupPath: PropTypes.string.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
startTime: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default About;
|
52
frontend/src/System/Status/About/AboutConnector.js
Normal file
52
frontend/src/System/Status/About/AboutConnector.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import About from './About';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.status,
|
||||
createUISettingsSelector(),
|
||||
(status, uiSettings) => {
|
||||
return {
|
||||
...status.item,
|
||||
timeFormat: uiSettings.timeFormat,
|
||||
longDateFormat: uiSettings.longDateFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchStatus
|
||||
};
|
||||
|
||||
class AboutConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchStatus();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<About
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AboutConnector.propTypes = {
|
||||
fetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AboutConnector);
|
93
frontend/src/System/Status/About/StartTime.js
Normal file
93
frontend/src/System/Status/About/StartTime.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
|
||||
function getUptime(startTime) {
|
||||
return formatTimeSpan(moment().diff(startTime));
|
||||
}
|
||||
|
||||
class StartTime extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const {
|
||||
startTime,
|
||||
timeFormat,
|
||||
longDateFormat
|
||||
} = props;
|
||||
|
||||
this._timeoutId = null;
|
||||
|
||||
this.state = {
|
||||
uptime: getUptime(startTime),
|
||||
startTime: formatDateTime(startTime, longDateFormat, timeFormat, { includeSeconds: true })
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._timeoutId = setTimeout(this.onTimeout, 1000);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
startTime,
|
||||
timeFormat,
|
||||
longDateFormat
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
startTime !== prevProps.startTime ||
|
||||
timeFormat !== prevProps.timeFormat ||
|
||||
longDateFormat !== prevProps.longDateFormat
|
||||
) {
|
||||
this.setState({
|
||||
uptime: getUptime(startTime),
|
||||
startTime: formatDateTime(startTime, longDateFormat, timeFormat, { includeSeconds: true })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._timeoutId) {
|
||||
this._timeoutId = clearTimeout(this._timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTimeout = () => {
|
||||
this.setState({ uptime: getUptime(this.props.startTime) });
|
||||
this._timeoutId = setTimeout(this.onTimeout, 1000);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
uptime,
|
||||
startTime
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<span title={startTime}>
|
||||
{uptime}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StartTime.propTypes = {
|
||||
startTime: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default StartTime;
|
5
frontend/src/System/Status/DiskSpace/DiskSpace.css
Normal file
5
frontend/src/System/Status/DiskSpace/DiskSpace.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.space {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 150px;
|
||||
}
|
120
frontend/src/System/Status/DiskSpace/DiskSpace.js
Normal file
120
frontend/src/System/Status/DiskSpace/DiskSpace.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import styles from './DiskSpace.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'path',
|
||||
label: 'Location',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'freeSpace',
|
||||
label: 'Free Space',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'totalSpace',
|
||||
label: 'Total Space',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'progress',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class DiskSpace extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FieldSet legend="Disk Space">
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching &&
|
||||
<Table
|
||||
columns={columns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
const {
|
||||
freeSpace,
|
||||
totalSpace
|
||||
} = item;
|
||||
|
||||
const diskUsage = (100 - freeSpace / totalSpace * 100);
|
||||
let diskUsageKind = kinds.PRIMARY;
|
||||
|
||||
if (diskUsage > 90) {
|
||||
diskUsageKind = kinds.DANGER;
|
||||
} else if (diskUsage > 80) {
|
||||
diskUsageKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow key={item.path}>
|
||||
<TableRowCell>
|
||||
{item.path}
|
||||
|
||||
{
|
||||
item.label &&
|
||||
` (${item.label})`
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.space}>
|
||||
{formatBytes(freeSpace)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.space}>
|
||||
{formatBytes(totalSpace)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.space}>
|
||||
<ProgressBar
|
||||
progress={diskUsage}
|
||||
kind={diskUsageKind}
|
||||
size={sizes.MEDIUM}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DiskSpace.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
items: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default DiskSpace;
|
54
frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js
Normal file
54
frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDiskSpace } from 'Store/Actions/systemActions';
|
||||
import DiskSpace from './DiskSpace';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.diskSpace,
|
||||
(diskSpace) => {
|
||||
const {
|
||||
isFetching,
|
||||
items
|
||||
} = diskSpace;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
items
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchDiskSpace
|
||||
};
|
||||
|
||||
class DiskSpaceConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchDiskSpace();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DiskSpace
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DiskSpaceConnector.propTypes = {
|
||||
fetchDiskSpace: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(DiskSpaceConnector);
|
21
frontend/src/System/Status/Health/Health.css
Normal file
21
frontend/src/System/Status/Health/Health.css
Normal file
@@ -0,0 +1,21 @@
|
||||
.legend {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.loading {
|
||||
composes: loading from 'Components/Loading/LoadingIndicator.css';
|
||||
|
||||
margin-top: 2px;
|
||||
margin-left: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.status {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.healthOk {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
206
frontend/src/System/Status/Health/Health.js
Normal file
206
frontend/src/System/Status/Health/Health.js
Normal file
@@ -0,0 +1,206 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import styles from './Health.css';
|
||||
|
||||
function getInternalLink(source) {
|
||||
switch (source) {
|
||||
case 'IndexerRssCheck':
|
||||
case 'IndexerSearchCheck':
|
||||
case 'IndexerStatusCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.SETTINGS}
|
||||
title="Settings"
|
||||
to="/settings/indexers"
|
||||
/>
|
||||
);
|
||||
case 'DownloadClientCheck':
|
||||
case 'ImportMechanismCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.SETTINGS}
|
||||
title="Settings"
|
||||
to="/settings/downloadclients"
|
||||
/>
|
||||
);
|
||||
case 'RootFolderCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.PLAY}
|
||||
title="Movie Editor"
|
||||
to="/movieeditor"
|
||||
/>
|
||||
);
|
||||
case 'UpdateCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.UPDATE}
|
||||
title="Updates"
|
||||
to="/system/updates"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function getTestLink(source, props) {
|
||||
switch (source) {
|
||||
case 'IndexerStatusCheck':
|
||||
return (
|
||||
<SpinnerIconButton
|
||||
name={icons.TEST}
|
||||
title="Test All"
|
||||
isSpinning={props.isTestingAllIndexers}
|
||||
onPress={props.dispatchTestAllIndexers}
|
||||
/>
|
||||
);
|
||||
case 'DownloadClientCheck':
|
||||
return (
|
||||
<SpinnerIconButton
|
||||
name={icons.TEST}
|
||||
title="Test All"
|
||||
isSpinning={props.isTestingAllDownloadClients}
|
||||
onPress={props.dispatchTestAllDownloadClients}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
className: styles.status,
|
||||
name: 'type',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
label: 'Message',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: 'Actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class Health extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const healthIssues = !!items.length;
|
||||
|
||||
return (
|
||||
<FieldSet
|
||||
legend={
|
||||
<div className={styles.legend}>
|
||||
Health
|
||||
|
||||
{
|
||||
isFetching && isPopulated &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!healthIssues &&
|
||||
<div className={styles.healthOk}>
|
||||
No issues with your configuration
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
healthIssues &&
|
||||
<Table
|
||||
columns={columns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
const internalLink = getInternalLink(item.source);
|
||||
const testLink = getTestLink(item.source, this.props);
|
||||
|
||||
return (
|
||||
<TableRow key={`health${item.message}`}>
|
||||
<TableRowCell>
|
||||
<Icon
|
||||
name={icons.DANGER}
|
||||
kind={item.type.toLowerCase() === 'error' ? kinds.DANGER : kinds.WARNING}
|
||||
title={titleCase(item.type)}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>{item.message}</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<IconButton
|
||||
name={icons.WIKI}
|
||||
to={item.wikiUrl}
|
||||
title="Read the Wiki for more information"
|
||||
/>
|
||||
|
||||
{
|
||||
internalLink
|
||||
}
|
||||
|
||||
{
|
||||
!!testLink &&
|
||||
testLink
|
||||
}
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Health.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
items: PropTypes.array.isRequired,
|
||||
isTestingAllDownloadClients: PropTypes.bool.isRequired,
|
||||
isTestingAllIndexers: PropTypes.bool.isRequired,
|
||||
dispatchTestAllDownloadClients: PropTypes.func.isRequired,
|
||||
dispatchTestAllIndexers: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Health;
|
68
frontend/src/System/Status/Health/HealthConnector.js
Normal file
68
frontend/src/System/Status/Health/HealthConnector.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchHealth } from 'Store/Actions/systemActions';
|
||||
import { testAllDownloadClients, testAllIndexers } from 'Store/Actions/settingsActions';
|
||||
import Health from './Health';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.health,
|
||||
(state) => state.settings.downloadClients.isTestingAll,
|
||||
(state) => state.settings.indexers.isTestingAll,
|
||||
(health, isTestingAllDownloadClients, isTestingAllIndexers) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
items
|
||||
} = health;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
items,
|
||||
isTestingAllDownloadClients,
|
||||
isTestingAllIndexers
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchHealth: fetchHealth,
|
||||
dispatchTestAllDownloadClients: testAllDownloadClients,
|
||||
dispatchTestAllIndexers: testAllIndexers
|
||||
};
|
||||
|
||||
class HealthConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchHealth();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchFetchHealth,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Health
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HealthConnector.propTypes = {
|
||||
dispatchFetchHealth: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(HealthConnector);
|
79
frontend/src/System/Status/Health/HealthStatusConnector.js
Normal file
79
frontend/src/System/Status/Health/HealthStatusConnector.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchHealth } from 'Store/Actions/systemActions';
|
||||
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.app,
|
||||
(state) => state.system.health,
|
||||
(app, health) => {
|
||||
const count = health.items.length;
|
||||
let errors = false;
|
||||
let warnings = false;
|
||||
|
||||
health.items.forEach((item) => {
|
||||
if (item.type === 'error') {
|
||||
errors = true;
|
||||
}
|
||||
|
||||
if (item.type === 'warning') {
|
||||
warnings = true;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
isConnected: app.isConnected,
|
||||
isReconnecting: app.isReconnecting,
|
||||
isPopulated: health.isPopulated,
|
||||
count,
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchHealth
|
||||
};
|
||||
|
||||
class HealthStatusConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.fetchHealth();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.isConnected && prevProps.isReconnecting) {
|
||||
this.props.fetchHealth();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageSidebarStatus
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HealthStatusConnector.propTypes = {
|
||||
isConnected: PropTypes.bool.isRequired,
|
||||
isReconnecting: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
fetchHealth: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(HealthStatusConnector);
|
52
frontend/src/System/Status/MoreInfo/MoreInfo.js
Normal file
52
frontend/src/System/Status/MoreInfo/MoreInfo.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { Component } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||
|
||||
class MoreInfo extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FieldSet legend="More Info">
|
||||
<DescriptionList>
|
||||
<DescriptionListItemTitle>Home page</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://radarr.video/">radarr.video</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>Wiki</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://github.com/Radarr/Radarr/wiki">github.com/Radarr/Radarr/wiki</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>Donations</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://radarr.video/donate">radarr.video/donate</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>Source</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://github.com/Radarr/Radarr/">github.com/Radarr/Radarr</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>Feature Requests</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://github.com/Radarr/Radarr/issues">github.com/Radarr/Radarr/issues</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
</DescriptionList>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MoreInfo.propTypes = {
|
||||
|
||||
};
|
||||
|
||||
export default MoreInfo;
|
29
frontend/src/System/Status/Status.js
Normal file
29
frontend/src/System/Status/Status.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import React, { Component } from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import HealthConnector from './Health/HealthConnector';
|
||||
import DiskSpaceConnector from './DiskSpace/DiskSpaceConnector';
|
||||
import AboutConnector from './About/AboutConnector';
|
||||
import MoreInfo from './MoreInfo/MoreInfo';
|
||||
|
||||
class Status extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageContent title="Status">
|
||||
<PageContentBodyConnector>
|
||||
<HealthConnector />
|
||||
<DiskSpaceConnector />
|
||||
<AboutConnector />
|
||||
<MoreInfo />
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Status;
|
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;
|
||||
}
|
265
frontend/src/System/Tasks/Queued/QueuedTaskRow.js
Normal file
265
frontend/src/System/Tasks/Queued/QueuedTaskRow.js
Normal file
@@ -0,0 +1,265 @@
|
||||
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
|
||||
title="Removed from task queue"
|
||||
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);
|
18
frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.css
Normal file
18
frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.css
Normal file
@@ -0,0 +1,18 @@
|
||||
.interval {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.lastExecution,
|
||||
.nextExecution {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 20px;
|
||||
}
|
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;
|
@@ -0,0 +1,92 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
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 ScheduledTaskRow from './ScheduledTaskRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { taskName }) => taskName,
|
||||
createCommandsSelector(),
|
||||
createUISettingsSelector(),
|
||||
(taskName, commands, uiSettings) => {
|
||||
const command = findCommand(commands, { name: taskName });
|
||||
|
||||
return {
|
||||
isQueued: !!(command && command.state === 'queued'),
|
||||
isExecuting: isCommandExecuting(command),
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
const taskName = props.taskName;
|
||||
|
||||
return {
|
||||
dispatchFetchTask() {
|
||||
dispatch(fetchTask({
|
||||
id: props.id
|
||||
}));
|
||||
},
|
||||
|
||||
onExecutePress() {
|
||||
dispatch(executeCommand({
|
||||
name: taskName
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class ScheduledTaskRowConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isExecuting,
|
||||
dispatchFetchTask
|
||||
} = this.props;
|
||||
|
||||
if (!isExecuting && prevProps.isExecuting) {
|
||||
// Give the host a moment to update after the command completes
|
||||
setTimeout(() => {
|
||||
dispatchFetchTask();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchFetchTask,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ScheduledTaskRow
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ScheduledTaskRowConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
isExecuting: PropTypes.bool.isRequired,
|
||||
dispatchFetchTask: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
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;
|
@@ -0,0 +1,46 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchTasks } from 'Store/Actions/systemActions';
|
||||
import ScheduledTasks from './ScheduledTasks';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.tasks,
|
||||
(tasks) => {
|
||||
return tasks;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchTasks: fetchTasks
|
||||
};
|
||||
|
||||
class ScheduledTasksConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchTasks();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ScheduledTasks
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ScheduledTasksConnector.propTypes = {
|
||||
dispatchFetchTasks: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ScheduledTasksConnector);
|
18
frontend/src/System/Tasks/Tasks.js
Normal file
18
frontend/src/System/Tasks/Tasks.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
|
||||
import QueuedTasksConnector from './Queued/QueuedTasksConnector';
|
||||
|
||||
function Tasks() {
|
||||
return (
|
||||
<PageContent title="Tasks">
|
||||
<PageContentBodyConnector>
|
||||
<ScheduledTasksConnector />
|
||||
<QueuedTasksConnector />
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tasks;
|
4
frontend/src/System/Updates/UpdateChanges.css
Normal file
4
frontend/src/System/Updates/UpdateChanges.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.title {
|
||||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
}
|
45
frontend/src/System/Updates/UpdateChanges.js
Normal file
45
frontend/src/System/Updates/UpdateChanges.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import styles from './UpdateChanges.css';
|
||||
|
||||
class UpdateChanges extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
changes
|
||||
} = this.props;
|
||||
|
||||
if (changes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<ul>
|
||||
{
|
||||
changes.map((change, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
{change}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UpdateChanges.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
changes: PropTypes.arrayOf(PropTypes.string)
|
||||
};
|
||||
|
||||
export default UpdateChanges;
|
57
frontend/src/System/Updates/Updates.css
Normal file
57
frontend/src/System/Updates/Updates.css
Normal file
@@ -0,0 +1,57 @@
|
||||
.updateAvailable {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.upToDate {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.upToDateIcon {
|
||||
color: #37bc9b;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.upToDateMessage {
|
||||
padding-left: 5px;
|
||||
font-size: 18px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
composes: loading from 'Components/Loading/LoadingIndicator.css';
|
||||
|
||||
margin-top: 5px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.update {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.space {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.branch {
|
||||
composes: label from 'Components/Label.css';
|
||||
|
||||
margin-left: 10px;
|
||||
font-size: 14px;
|
||||
}
|
169
frontend/src/System/Updates/Updates.js
Normal file
169
frontend/src/System/Updates/Updates.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import UpdateChanges from './UpdateChanges';
|
||||
import styles from './Updates.css';
|
||||
|
||||
class Updates extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
isInstallingUpdate,
|
||||
shortDateFormat,
|
||||
onInstallLatestPress
|
||||
} = this.props;
|
||||
|
||||
const hasUpdates = isPopulated && !error && items.length > 0;
|
||||
const noUpdates = isPopulated && !error && !items.length;
|
||||
const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true });
|
||||
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
|
||||
|
||||
return (
|
||||
<PageContent title="Updates">
|
||||
<PageContentBodyConnector>
|
||||
{
|
||||
!isPopulated && !error &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
noUpdates &&
|
||||
<div>No updates are available</div>
|
||||
}
|
||||
|
||||
{
|
||||
hasUpdateToInstall &&
|
||||
<div className={styles.updateAvailable}>
|
||||
<SpinnerButton
|
||||
className={styles.updateAvailable}
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isInstallingUpdate}
|
||||
onPress={onInstallLatestPress}
|
||||
>
|
||||
Install Latest
|
||||
</SpinnerButton>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
noUpdateToInstall &&
|
||||
<div className={styles.upToDate}>
|
||||
<Icon
|
||||
className={styles.upToDateIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={30}
|
||||
/>
|
||||
<div className={styles.upToDateMessage}>
|
||||
The latest version of Radarr is already installed
|
||||
</div>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
hasUpdates &&
|
||||
<div>
|
||||
{
|
||||
items.map((update) => {
|
||||
const hasChanges = !!update.changes;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={update.version}
|
||||
className={styles.update}
|
||||
>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.version}>{update.version}</div>
|
||||
<div className={styles.space}>—</div>
|
||||
<div className={styles.date}>{formatDate(update.releaseDate, shortDateFormat)}</div>
|
||||
|
||||
{
|
||||
update.branch !== 'master' &&
|
||||
<Label
|
||||
className={styles.branch}
|
||||
>
|
||||
{update.branch}
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
!hasChanges &&
|
||||
<div>Maintenance release</div>
|
||||
}
|
||||
|
||||
{
|
||||
hasChanges &&
|
||||
<div className={styles.changes}>
|
||||
<UpdateChanges
|
||||
title="New"
|
||||
changes={update.changes.new}
|
||||
/>
|
||||
|
||||
<UpdateChanges
|
||||
title="Fixed"
|
||||
changes={update.changes.fixed}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!!error &&
|
||||
<div>
|
||||
Failed to fetch updates
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Updates.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.array.isRequired,
|
||||
isInstallingUpdate: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
onInstallLatestPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Updates;
|
76
frontend/src/System/Updates/UpdatesConnector.js
Normal file
76
frontend/src/System/Updates/UpdatesConnector.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchUpdates } from 'Store/Actions/systemActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Updates from './Updates';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.updates,
|
||||
createUISettingsSelector(),
|
||||
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE),
|
||||
(updates, uiSettings, isInstallingUpdate) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = updates;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
isInstallingUpdate,
|
||||
shortDateFormat: uiSettings.shortDateFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchUpdates,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class UpdatesConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchUpdates();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInstallLatestPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.APPLICATION_UPDATE });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Updates
|
||||
onInstallLatestPress={this.onInstallLatestPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UpdatesConnector.propTypes = {
|
||||
fetchUpdates: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(UpdatesConnector);
|
Reference in New Issue
Block a user