Initial Commit Rework

This commit is contained in:
Qstick 2017-09-03 22:20:56 -04:00
parent 74a4cc048c
commit 95051cbd63
2483 changed files with 101351 additions and 111396 deletions

View file

@ -0,0 +1,5 @@
.type {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 20px;
}

View file

@ -0,0 +1,148 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
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 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 styles from './Backups.css';
const columns = [
{
name: 'type',
isVisible: true
},
{
name: 'name',
label: 'Name',
isVisible: true
},
{
name: 'time',
label: 'Time',
isVisible: true
}
];
class Backups extends Component {
//
// Render
render() {
const {
isFetching,
items,
backupExecuting,
onBackupPress
} = this.props;
const hasBackups = !isFetching && items.length > 0;
const noBackups = !isFetching && !items.length;
return (
<PageContent title="Backups">
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Backup Now"
iconName={icons.BACKUP}
isSpinning={backupExecuting}
onPress={onBackupPress}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>
{
isFetching &&
<LoadingIndicator />
}
{
noBackups &&
<div>No backups are available</div>
}
{
hasBackups &&
<Table
columns={columns}
>
<TableBody>
{
items.map((item) => {
const {
id,
type,
name,
path,
time
} = item;
let iconClassName = icons.SCHEDULED;
let iconTooltip = 'Scheduled';
if (type === 'manual') {
iconClassName = icons.INTERACTIVE;
iconTooltip = 'Manual';
} else if (item === '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
className={styles.time}
date={time}
/>
</TableRow>
);
})
}
</TableBody>
</Table>
}
</PageContentBodyConnector>
</PageContent>
);
}
}
Backups.propTypes = {
isFetching: PropTypes.bool.isRequired,
items: PropTypes.array.isRequired,
backupExecuting: PropTypes.bool.isRequired,
onBackupPress: PropTypes.func.isRequired
};
export default Backups;

View file

@ -0,0 +1,81 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import { fetchBackups } 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,
createCommandsSelector(),
(backups, commands) => {
const {
isFetching,
items
} = backups;
const backupExecuting = _.some(commands, { name: commandNames.BACKUP });
return {
isFetching,
items,
backupExecuting
};
}
);
}
const mapDispatchToProps = {
fetchBackups,
executeCommand
};
class BackupsConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchBackups();
}
componentDidUpdate(prevProps) {
if (prevProps.backupExecuting && !this.props.backupExecuting) {
this.props.fetchBackups();
}
}
//
// Listeners
onBackupPress = () => {
this.props.executeCommand({
name: commandNames.BACKUP
});
}
//
// Render
render() {
return (
<Backups
onBackupPress={this.onBackupPress}
{...this.props}
/>
);
}
}
BackupsConnector.propTypes = {
backupExecuting: PropTypes.bool.isRequired,
fetchBackups: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(BackupsConnector);

View file

@ -0,0 +1,176 @@
import PropTypes from 'prop-types';
import React, { Component } 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 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 FilterMenuItem from 'Components/Menu/FilterMenuItem';
import MenuContent from 'Components/Menu/MenuContent';
import LogsTableRow from './LogsTableRow';
class LogsTable extends Component {
//
// Listeners
onFilterMenuItemPress = (filterKey, filterValue) => {
this.props.onFilterSelect(filterKey, filterValue);
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
columns,
filterKey,
filterValue,
totalRecords,
clearLogExecuting,
onRefreshPress,
onClearLogsPress,
...otherProps
} = this.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}>
<FilterMenu alignMenu={align.RIGHT}>
<MenuContent>
<FilterMenuItem
filterKey={filterKey}
filterValue={filterValue}
onPress={this.onFilterMenuItemPress}
>
All
</FilterMenuItem>
<FilterMenuItem
name="level"
value="Info"
filterKey={filterKey}
filterValue={filterValue}
onPress={this.onFilterMenuItemPress}
>
Info
</FilterMenuItem>
<FilterMenuItem
name="level"
value="Warn"
filterKey={filterKey}
filterValue={filterValue}
onPress={this.onFilterMenuItemPress}
>
Warn
</FilterMenuItem>
<FilterMenuItem
name="level"
value="Error"
filterKey={filterKey}
filterValue={filterValue}
onPress={this.onFilterMenuItemPress}
>
Error
</FilterMenuItem>
</MenuContent>
</FilterMenu>
</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,
filterKey: PropTypes.string,
filterValue: PropTypes.string,
totalRecords: PropTypes.number,
clearLogExecuting: PropTypes.bool.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onRefreshPress: PropTypes.func.isRequired,
onClearLogsPress: PropTypes.func.isRequired
};
export default LogsTable;

View file

@ -0,0 +1,130 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
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,
createCommandsSelector(),
(logs, commands) => {
const clearLogExecuting = _.some(commands, { name: commandNames.CLEAR_LOGS });
return {
clearLogExecuting,
...logs
};
}
);
}
const mapDispatchToProps = {
executeCommand,
...systemActions
};
class LogsTableConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchLogs();
}
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 = (filterKey, filterValue) => {
this.props.setLogsFilter({ filterKey, filterValue });
}
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 = {
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 connect(createMapStateToProps, mapDispatchToProps)(LogsTableConnector);

View 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;
}

View 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;

View 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;
}

View file

@ -0,0 +1,150 @@
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 = () => {
this.setState({ isDetailsModalOpen: true });
}
onModalClose = () => {
this.setState({ isDetailsModalOpen: false });
}
//
// Render
render() {
const {
level,
logger,
message,
time,
exception,
columns
} = this.props;
return (
<TableRowButton 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.toLowerCase()]}
name={getIconName(level.toLowerCase())}
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}
/>
);
}
})
}
<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;

View 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;

View file

@ -0,0 +1,93 @@
import _ from 'lodash';
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 createCommandsSelector from 'Store/Selectors/createCommandsSelector';
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,
createCommandsSelector(),
(logFiles, status, commands) => {
const {
isFetching,
items
} = logFiles;
const {
appData,
isWindows
} = status;
const deleteFilesExecuting = _.some(commands, { name: commandNames.DELETE_LOG_FILES });
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);

View file

@ -0,0 +1,5 @@
.download {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 100px;
}

View 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;

View 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;

View 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;

View file

@ -0,0 +1,93 @@
import _ from 'lodash';
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 createCommandsSelector from 'Store/Selectors/createCommandsSelector';
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,
createCommandsSelector(),
(updateLogFiles, status, commands) => {
const {
isFetching,
items
} = updateLogFiles;
const deleteFilesExecuting = _.some(commands, { name: commandNames.DELETE_UPDATE_LOG_FILES });
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);

View file

@ -0,0 +1,71 @@
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';
class About extends Component {
//
// Render
render() {
const {
version,
isMonoRuntime,
runtimeVersion,
appData,
startupPath,
mode
} = this.props;
return (
<FieldSet
legend="About"
>
<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)}
/>
</DescriptionList>
</FieldSet>
);
}
}
About.propTypes = {
version: PropTypes.string,
isMonoRuntime: PropTypes.bool,
runtimeVersion: PropTypes.string,
appData: PropTypes.string,
startupPath: PropTypes.string,
mode: PropTypes.string
};
export default About;

View file

@ -0,0 +1,48 @@
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 About from './About';
function createMapStateToProps() {
return createSelector(
(state) => state.system.status,
(status) => {
return {
...status.item
};
}
);
}
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);

View file

@ -0,0 +1,5 @@
.space {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 150px;
}

View file

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

View 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);

View 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;
}

View file

@ -0,0 +1,170 @@
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 Link from 'Components/Link/Link';
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 (
<Link to="/settings/indexers">
Settings
</Link>
);
case 'DownloadClientCheck':
case 'ImportMechanismCheck':
return (
<Link to="/settings/downloadclients">
Settings
</Link>
);
case 'RootFolderCheck':
return (
<div>
<Link to="/serieseditor">
Series Editor
</Link>
</div>
);
case 'UpdateCheck':
return (
<Link to="/system/updates">
Updates
</Link>
);
default:
return;
}
}
const columns = [
{
className: styles.status,
name: 'type',
isVisible: true
},
{
name: 'message',
label: 'Message',
isVisible: true
},
{
name: 'wikiLink',
label: 'Wiki',
isVisible: true
},
{
name: 'internalLink',
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);
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>
<Link
to={item.wikiUrl}
title="Read the Wiki for more information"
>
Wiki
</Link>
</TableRowCell>
<TableRowCell>
{
internalLink
}
</TableRowCell>
</TableRow>
);
})
}
</TableBody>
</Table>
}
</FieldSet>
);
}
}
Health.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
items: PropTypes.array.isRequired
};
export default Health;

View file

@ -0,0 +1,56 @@
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 Health from './Health';
function createMapStateToProps() {
return createSelector(
(state) => state.system.health,
(health) => {
const {
isFetching,
isPopulated,
items
} = health;
return {
isFetching,
isPopulated,
items
};
}
);
}
const mapDispatchToProps = {
fetchHealth
};
class HealthConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchHealth();
}
//
// Render
render() {
return (
<Health
{...this.props}
/>
);
}
}
HealthConnector.propTypes = {
fetchHealth: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(HealthConnector);

View 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);

View file

@ -0,0 +1,69 @@
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://lidarr.audio/">lidarr.audio</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>Wiki</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://github.com/Lidarr/Lidarr/Wiki">wiki.lidarr.audio</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>Reddit</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://www.reddit.com/r/Lidarr/">Lidarr</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>Discord</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://discord.gg/8Y7rDc9">#lidarr on Discord</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>Donations</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://www.paypal.me/Lidarr">Donate to Lidarr</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>Donations (Sonarr)</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="ttps://sonarr.tv/donate">Donate to Sonarr</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>Source</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://github.com/Lidarr/Lidarr/">github.com/Lidarr/Lidarr</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>Feature Requests</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://github.com/Lidarr/Lidarr/issues">github.com/Lidarr/Lidarr/issues</Link>
</DescriptionListItemDescription>
</DescriptionList>
</FieldSet>
);
}
}
MoreInfo.propTypes = {
};
export default MoreInfo;

View 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;

View 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;
}

View file

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

View file

@ -0,0 +1,91 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { findCommand } from 'Utilities/Command';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchTask } from 'Store/Actions/systemActions';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import TaskRow from './TaskRow';
function createMapStateToProps() {
return createSelector(
(state, { taskName }) => taskName,
createCommandsSelector(),
createUISettingsSelector(),
(taskName, commands, uiSettings) => {
const isExecuting = !!findCommand(commands, { name: taskName });
return {
isExecuting,
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 TaskRowConnector 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 (
<TaskRow
{...otherProps}
/>
);
}
}
TaskRowConnector.propTypes = {
id: PropTypes.number.isRequired,
isExecuting: PropTypes.bool.isRequired,
dispatchFetchTask: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(TaskRowConnector);

View file

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

View file

@ -0,0 +1,46 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchTasks } from 'Store/Actions/systemActions';
import Tasks from './Tasks';
function createMapStateToProps() {
return createSelector(
(state) => state.system.tasks,
(tasks) => {
return tasks;
}
);
}
const mapDispatchToProps = {
fetchTasks
};
class TasksConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchTasks();
}
//
// Render
render() {
return (
<Tasks
{...this.props}
/>
);
}
}
TasksConnector.propTypes = {
fetchTasks: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(TasksConnector);

View file

@ -0,0 +1,4 @@
.title {
margin-top: 10px;
font-size: 16px;
}

View 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

View file

@ -0,0 +1,46 @@
.upToDate {
display: flex;
margin-bottom: 20px;
}
.upToDateIcon {
color: #37bc9b;
font-size: 30px;
}
.upToDateMessage {
padding-left: 5px;
font-size: 18px;
line-height: 30px;
}
.update {
margin-top: 20px;
}
.info {
display: flex;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #e5e5e5;
line-height: 21px;
}
.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;
}

View file

@ -0,0 +1,149 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { 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 {
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 &&
<LoadingIndicator />
}
{
noUpdates &&
<div>No updates are available</div>
}
{
hasUpdateToInstall &&
<SpinnerButton
kind={kinds.PRIMARY}
className={styles.updateAvailable}
isSpinning={isInstallingUpdate}
onPress={onInstallLatestPress}
>
Install Latest
</SpinnerButton>
}
{
noUpdateToInstall &&
<div className={styles.upToDate}>
<Icon
className={styles.upToDateIcon}
name="fa fa-check-circle"
size={30}
/>
<div className={styles.upToDateMessage}>
The latest version of Sonarr is already installed
</div>
</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}>&mdash;</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 className={styles.upToDateMessage}>
Failed to fetch updates
</div>
}
</PageContentBodyConnector>
</PageContent>
);
}
}
Updates.propTypes = {
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object.isRequired,
items: PropTypes.array.isRequired,
isInstallingUpdate: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
onInstallLatestPress: PropTypes.func.isRequired
};
export default Updates;

View file

@ -0,0 +1,74 @@
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 {
isPopulated,
error,
items
} = updates;
return {
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);