mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-08 05:51:47 -07:00
Initial Commit Rework
This commit is contained in:
parent
74a4cc048c
commit
95051cbd63
2483 changed files with 101351 additions and 111396 deletions
5
frontend/src/System/Backup/Backups.css
Normal file
5
frontend/src/System/Backup/Backups.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
.type {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 20px;
|
||||
}
|
148
frontend/src/System/Backup/Backups.js
Normal file
148
frontend/src/System/Backup/Backups.js
Normal 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;
|
81
frontend/src/System/Backup/BackupsConnector.js
Normal file
81
frontend/src/System/Backup/BackupsConnector.js
Normal 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);
|
176
frontend/src/System/Events/LogsTable.js
Normal file
176
frontend/src/System/Events/LogsTable.js
Normal 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;
|
130
frontend/src/System/Events/LogsTableConnector.js
Normal file
130
frontend/src/System/Events/LogsTableConnector.js
Normal 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);
|
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;
|
||||
}
|
150
frontend/src/System/Events/LogsTableRow.js
Normal file
150
frontend/src/System/Events/LogsTableRow.js
Normal 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;
|
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;
|
93
frontend/src/System/Logs/Files/LogFilesConnector.js
Normal file
93
frontend/src/System/Logs/Files/LogFilesConnector.js
Normal 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);
|
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;
|
93
frontend/src/System/Logs/Updates/UpdateLogFilesConnector.js
Normal file
93
frontend/src/System/Logs/Updates/UpdateLogFilesConnector.js
Normal 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);
|
71
frontend/src/System/Status/About/About.js
Normal file
71
frontend/src/System/Status/About/About.js
Normal 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;
|
48
frontend/src/System/Status/About/AboutConnector.js
Normal file
48
frontend/src/System/Status/About/AboutConnector.js
Normal 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);
|
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;
|
||||
}
|
122
frontend/src/System/Status/DiskSpace/DiskSpace.js
Normal file
122
frontend/src/System/Status/DiskSpace/DiskSpace.js
Normal 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;
|
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;
|
||||
}
|
||||
|
170
frontend/src/System/Status/Health/Health.js
Normal file
170
frontend/src/System/Status/Health/Health.js
Normal 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;
|
56
frontend/src/System/Status/Health/HealthConnector.js
Normal file
56
frontend/src/System/Status/Health/HealthConnector.js
Normal 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);
|
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);
|
69
frontend/src/System/Status/MoreInfo/MoreInfo.js
Normal file
69
frontend/src/System/Status/MoreInfo/MoreInfo.js
Normal 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;
|
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;
|
18
frontend/src/System/Tasks/TaskRow.css
Normal file
18
frontend/src/System/Tasks/TaskRow.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;
|
||||
}
|
94
frontend/src/System/Tasks/TaskRow.js
Normal file
94
frontend/src/System/Tasks/TaskRow.js
Normal 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;
|
91
frontend/src/System/Tasks/TaskRowConnector.js
Normal file
91
frontend/src/System/Tasks/TaskRowConnector.js
Normal 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);
|
89
frontend/src/System/Tasks/Tasks.js
Normal file
89
frontend/src/System/Tasks/Tasks.js
Normal 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;
|
46
frontend/src/System/Tasks/TasksConnector.js
Normal file
46
frontend/src/System/Tasks/TasksConnector.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 { 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);
|
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
|
46
frontend/src/System/Updates/Updates.css
Normal file
46
frontend/src/System/Updates/Updates.css
Normal 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;
|
||||
}
|
149
frontend/src/System/Updates/Updates.js
Normal file
149
frontend/src/System/Updates/Updates.js
Normal 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}>—</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;
|
74
frontend/src/System/Updates/UpdatesConnector.js
Normal file
74
frontend/src/System/Updates/UpdatesConnector.js
Normal 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);
|
Loading…
Add table
Add a link
Reference in a new issue