diff --git a/components/Buttons/UpdateButton.jsx b/components/Buttons/UpdateButton.jsx index 4e02b53d7..c79d5d5e6 100644 --- a/components/Buttons/UpdateButton.jsx +++ b/components/Buttons/UpdateButton.jsx @@ -10,22 +10,15 @@ class UpdateButton extends React.Component { updateStatus: PropTypes.object, }; - constructor(props) { - super(props); - this.handleClick = this.handleClick.bind(this); - this.handleAlertDismiss = this.handleAlertDismiss.bind(this); - this.reloadPage = this.reloadPage.bind(this); - } - - handleClick() { + static handleClick() { updateWebuiAsync(); } - handleAlertDismiss() { + static handleAlertDismiss() { updateWebui({ status: false }); } - reloadPage() { + static reloadPage() { history.go({ pathname: '/' }); } @@ -35,12 +28,12 @@ class UpdateButton extends React.Component { const { status, error } = this.props.updateStatus.items; const successAlert = ( - +

Update Successful!

Close this notification to reload.

); const errorAlert = ( - +

Oops! Something went wrong!

Submit an Issue on GitHub so we can fix it

{error.message}

@@ -50,7 +43,7 @@ class UpdateButton extends React.Component { return (
  • - + Interactive Log - +
  • diff --git a/components/TreeView/TreeNode.jsx b/components/TreeView/TreeNode.jsx index 42239dccb..64b6bef1d 100644 --- a/components/TreeView/TreeNode.jsx +++ b/components/TreeView/TreeNode.jsx @@ -53,7 +53,7 @@ class TreeNode extends React.Component { if (!loaded) { fetchApiFolder(basePath) - .then((response) => response.json()) + .then(response => response.json()) .then((json) => { const nodes = []; forEach(json.subdir, (item) => { @@ -61,7 +61,7 @@ class TreeNode extends React.Component { }); return nodes; }) - .then((nodes) => this.setState({ loaded: true, expanded: !expanded, nodes })); + .then(nodes => this.setState({ loaded: true, expanded: !expanded, nodes })); } else { this.setState({ expanded: !expanded }); } diff --git a/core/actions.js b/core/actions.js index 835370419..90ba49e34 100644 --- a/core/actions.js +++ b/core/actions.js @@ -1,8 +1,11 @@ import 'isomorphic-fetch'; import { createAction } from 'redux-actions'; import { forEach } from 'lodash'; +import objPath from 'object-path'; import store from './store'; import history from './history'; +import { getDeltaAsync } from './actions/logs/Delta'; +import { appendContents } from './actions/logs/Contents'; const VERSION = __VERSION__; // eslint-disable-line no-undef @@ -13,7 +16,7 @@ export const STATUS_RECEIVE = 'STATUS_RECEIVE'; export function createAsyncAction(type, key, apiAction, responseCallback) { return (forceFetch, apiParams = '') => { const state = store.getState(); - const status = state[key]; + const status = objPath.get(state, key); const apiKey = state.apiSession.apikey; let shouldFetch; @@ -120,7 +123,7 @@ export const updateWebuiAsync = createAsyncAction(WEBUI_VERSION_UPDATE, } return { status: true, error: new Error(`Response status: ${response.status}`) }; }); -export const updateWebui = createAction(WEBUI_VERSION_UPDATE, (payload) => ({ items: payload })); +export const updateWebui = createAction(WEBUI_VERSION_UPDATE, payload => ({ items: payload })); export const JMM_VERSION = 'JMM_VERSION'; export const jmmVersionAsync = createAsyncAction(JMM_VERSION, 'jmmVersion', '/version', (response) => { @@ -153,6 +156,17 @@ function autoUpdateTick() { if (location === '/dashboard') { queueStatusAsync(true); recentFilesAsync(true); + } else if (location === '/logs') { + const state = store.getState(); + const lines = 10; + let position = 0; + try { + position = state.logs.delta.items.position; + } catch (ex) { console.error('Unable to get log position'); } + getDeltaAsync(true, `${lines}/${position}`).then(() => { + const newState = store.getState(); + store.dispatch(appendContents(newState.logs.delta.items.lines)); + }); } } let autoupdateTimer = null; diff --git a/core/actions/logs/Contents.js b/core/actions/logs/Contents.js new file mode 100644 index 000000000..b7f409d51 --- /dev/null +++ b/core/actions/logs/Contents.js @@ -0,0 +1,6 @@ +import { createAction } from 'redux-actions'; + +export const SET_CONTENTS = 'LOGS_SET_CONTENTS'; +export const setContents = createAction(SET_CONTENTS); +export const APPEND_CONTENTS = 'LOGS_APPEND_CONTENTS'; +export const appendContents = createAction(APPEND_CONTENTS); diff --git a/core/actions/logs/Delta.js b/core/actions/logs/Delta.js new file mode 100644 index 000000000..6c25359e2 --- /dev/null +++ b/core/actions/logs/Delta.js @@ -0,0 +1,4 @@ +import { createAsyncAction } from '../../actions'; + +export const GET_DELTA = 'LOGS_GET_DELTA'; +export const getDeltaAsync = createAsyncAction(GET_DELTA, 'logs.delta', '/log/get/'); diff --git a/core/actions/settings/Log.js b/core/actions/settings/Log.js index d05b628bd..ce7bc1b6c 100644 --- a/core/actions/settings/Log.js +++ b/core/actions/settings/Log.js @@ -1,6 +1,6 @@ import { createAsyncAction, createAsyncPostAction } from '../../actions'; export const GET_LOG = 'SETTINGS_GET_LOG'; -export const getLog = createAsyncAction(GET_LOG, 'logs', '/log/rotate'); +export const getLog = createAsyncAction(GET_LOG, 'settings.logs', '/log/rotate'); export const SET_LOG = 'SETTINGS_SET_LOG'; -export const setLog = createAsyncPostAction(SET_LOG, 'setLogs', '/log/rotate'); +export const setLog = createAsyncPostAction(SET_LOG, 'settings.setLogs', '/log/rotate'); diff --git a/core/reducers.js b/core/reducers.js index 5d9cc6e38..b5367c2ff 100644 --- a/core/reducers.js +++ b/core/reducers.js @@ -21,6 +21,7 @@ import { } from './actions'; import modals from './reducers/modals'; import settings from './reducers/settings'; +import logs from './reducers/logs'; const VERSION = __VERSION__; // eslint-disable-line no-undef @@ -28,7 +29,7 @@ export function createApiReducer(type, dataPropName = 'items', dataPropValue = { valueFn = undefined) { let valueFunc = null; if (valueFn === undefined) { - valueFunc = (value) => value; + valueFunc = value => value; } else { valueFunc = valueFn; } @@ -64,12 +65,12 @@ export function createApiReducer(type, dataPropName = 'items', dataPropValue = { } const queueStatus = createApiReducer(QUEUE_STATUS); -const recentFiles = createApiReducer(RECENT_FILES); -const jmmNews = createApiReducer(JMM_NEWS); -const importFolders = createApiReducer(IMPORT_FOLDERS); +const recentFiles = createApiReducer(RECENT_FILES, 'items', []); +const jmmNews = createApiReducer(JMM_NEWS, 'items', []); +const importFolders = createApiReducer(IMPORT_FOLDERS, 'items', []); const seriesCount = createApiReducer(SERIES_COUNT); const filesCount = createApiReducer(FILES_COUNT); -const updateAvailable = createApiReducer(UPDATE_AVAILABLE, 'status', false, (payload) => +const updateAvailable = createApiReducer(UPDATE_AVAILABLE, 'status', false, payload => VERSION.indexOf('.') !== -1 && payload.version !== VERSION ); const webuiVersionUpdate = createApiReducer(WEBUI_VERSION_UPDATE, 'items', @@ -108,6 +109,7 @@ const rootReducer = combineReducers({ selectedImportFolderSeries, settings, modals, + logs, }); export default rootReducer; diff --git a/core/reducers/logs.js b/core/reducers/logs.js new file mode 100644 index 000000000..41f9a3344 --- /dev/null +++ b/core/reducers/logs.js @@ -0,0 +1,8 @@ +import { combineReducers } from 'redux'; +import { contents } from './logs/Contents'; +import { delta } from './logs/Delta'; + +export default combineReducers({ + contents, + delta, +}); diff --git a/core/reducers/logs/Contents.js b/core/reducers/logs/Contents.js new file mode 100644 index 000000000..c222110b6 --- /dev/null +++ b/core/reducers/logs/Contents.js @@ -0,0 +1,23 @@ +import { handleActions } from 'redux-actions'; +import { concat } from 'lodash'; +import { SET_CONTENTS, APPEND_CONTENTS } from '../../actions/logs/Contents'; + +const defaultState = { + lines: [], + position: 0, +}; + +export const contents = handleActions({ + [SET_CONTENTS]: (state, action) => { + if (action.error) { return state; } + const lines = action.payload.lines || []; + return Object.assign({}, state, { lines }); + }, + [APPEND_CONTENTS]: (state, action) => { + if (action.error) { return state; } + const lines = concat(state.lines, action.payload); + return Object.assign({}, state, { lines }); + }, +}, defaultState); + +export default {}; diff --git a/core/reducers/logs/Delta.js b/core/reducers/logs/Delta.js new file mode 100644 index 000000000..ec69bffc1 --- /dev/null +++ b/core/reducers/logs/Delta.js @@ -0,0 +1,6 @@ +import { createApiReducer } from '../../reducers'; +import { GET_DELTA } from '../../actions/logs/Delta'; + +export const delta = createApiReducer(GET_DELTA); + +export default {}; diff --git a/core/reducers/settings/Log.js b/core/reducers/settings/Log.js index 0e956587a..51e1ebe6e 100644 --- a/core/reducers/settings/Log.js +++ b/core/reducers/settings/Log.js @@ -1,10 +1,5 @@ -import { combineReducers } from 'redux'; import { createApiReducer } from '../../reducers'; import { GET_LOG, SET_LOG } from '../../actions/settings/Log'; export const logs = createApiReducer(GET_LOG); export const setLogs = createApiReducer(SET_LOG); - -export default combineReducers({ - logs, -}); diff --git a/core/router.jsx b/core/router.jsx index 15ae4305f..28233301b 100644 --- a/core/router.jsx +++ b/core/router.jsx @@ -30,7 +30,7 @@ function matchURI(route, path) { const params = Object.create(null); - for (let i = 1; i < match.length; i++) { + for (let i = 1; i < match.length; i += 1) { params[route.keys[i - 1].name] = match[i] !== undefined ? decodeParam(match[i]) : undefined; } @@ -54,7 +54,7 @@ function resolve(routes, context) { const keys = Object.keys(route.data); return Promise.all([ route.load(), - ...keys.map(key => { // eslint-disable-line no-loop-func + ...keys.map((key) => { // eslint-disable-line no-loop-func const query = route.data[key]; const method = query.substring(0, query.indexOf(' ')); // GET const url = query.substr(query.indexOf(' ') + 1); // /api/tasks/$id diff --git a/core/store.js b/core/store.jsx similarity index 100% rename from core/store.js rename to core/store.jsx diff --git a/css/main.css b/css/main.css index 38b0bf784..248dfb110 100644 --- a/css/main.css +++ b/css/main.css @@ -409,3 +409,6 @@ a.logo span { background-color: #fff; color: #b0b5b9; } +.log-panel .panel-body { + white-space: pre; +} \ No newline at end of file diff --git a/package.json b/package.json index 885dbd540..4544f28fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jmmserver-webui", - "version": "0.1.12-dev", + "version": "0.1.12-dev2", "private": true, "engines": { "node": ">=6", @@ -25,6 +25,7 @@ "isomorphic-fetch": "^1.0.0", "lodash": "^4.15.0", "moment": "^2.14.1", + "object-path": "^0.11.2", "open-sans-fontface": "^1.4.0", "path-to-regexp": "^1.5.3", "pixrem": "^3.0.1", @@ -66,12 +67,12 @@ "babel-preset-stage-1": "^6.5.0", "babel-register": "^6.11.6", "babel-runtime": "^6.11.6", - "css-loader": "^0.23.1", - "eslint": "^3.1.1", - "eslint-config-airbnb": "^10.0.0", - "eslint-plugin-import": "^1.12.0", + "css-loader": "^0.25.0", + "eslint": "^3.6.0", + "eslint-config-airbnb": "^12.0.0", + "eslint-plugin-import": "^1.16.0", "eslint-plugin-jsx-a11y": "^2.0.1", - "eslint-plugin-react": "^6.0.0", + "eslint-plugin-react": "^6.3.0", "file-loader": "^0.9.0", "font-awesome-webpack": "0.0.4", "jsdom": "^9.4.2", @@ -100,7 +101,10 @@ "extends": "airbnb", "rules": { "react/prefer-stateless-function": "off", - "react/jsx-filename-extension": "off" + "react/jsx-filename-extension": "off", + "jsx-a11y/no-static-element-interactions": "off", + "react/forbid-prop-types": "off", + "no-console": ["error", { "allow": ["warn", "error"] }] } }, "stylelint": { diff --git a/pages/error/index.jsx b/pages/error/index.jsx index 8c313e77f..98a89f3fc 100644 --- a/pages/error/index.jsx +++ b/pages/error/index.jsx @@ -15,7 +15,7 @@ class ErrorPage extends React.Component { 'Page Not Found' : 'Error'; } - goBack = event => { + goBack = (event) => { event.preventDefault(); history.goBack(); }; @@ -34,7 +34,7 @@ class ErrorPage extends React.Component {

    {title}

    {code === '404' &&

    - The page you're looking for does not exist or an another error occurred. + The page you're looking for does not exist or an another error occurred.

    }

    diff --git a/pages/import/ImportFolderSeries.jsx b/pages/import/ImportFolderSeries.jsx index c7973181c..9089cb4e6 100644 --- a/pages/import/ImportFolderSeries.jsx +++ b/pages/import/ImportFolderSeries.jsx @@ -20,17 +20,11 @@ class ImportFolderSeries extends React.Component { selectedFolder: PropTypes.object, }; - constructor(props) { - super(props); - this.handleClick = this.handleClick.bind(this); - this.handleSelect = this.handleSelect.bind(this); - } - - handleClick() { + static handleClick() { importFolderSeriesAsync(true, '/1'); } - handleSelect(folderId) { + static handleSelect(folderId) { const { importFolders } = store.getState(); const folder = find(importFolders.items, ['ImportFolderID', folderId]); store.dispatch( @@ -45,7 +39,7 @@ class ImportFolderSeries extends React.Component { const folders = []; let i = 0; forEach(items, (item) => { - i++; + i += 1; series.push(); }); @@ -59,10 +53,10 @@ class ImportFolderSeries extends React.Component { Series In Import Folder - {folders} + {folders} , ]; @@ -75,11 +69,11 @@ class ImportFolderSeries extends React.Component { lastUpdated={lastUpdated} isFetching={isFetching} actionName="Sort" - onAction={this.handleClick} + onAction={ImportFolderSeries.handleClick} > - {series} + {series}
    diff --git a/pages/import/ImportFolderSeriesItem.jsx b/pages/import/ImportFolderSeriesItem.jsx index 1f9812524..9eb07d954 100644 --- a/pages/import/ImportFolderSeriesItem.jsx +++ b/pages/import/ImportFolderSeriesItem.jsx @@ -3,7 +3,6 @@ import prettysize from 'prettysize'; class ImportFolderSeriesItem extends React.Component { static propTypes = { - AnimeSeriesID: PropTypes.number, index: PropTypes.number, AnimeSeriesName: PropTypes.string, FileCount: PropTypes.number, diff --git a/pages/import/index.jsx b/pages/import/index.jsx index 59f0b6658..7bf38ce76 100644 --- a/pages/import/index.jsx +++ b/pages/import/index.jsx @@ -18,6 +18,7 @@ class ImportFoldersPage extends React.Component { history.push({ pathname: '/', }); + return; } } diff --git a/pages/logs/LogContents.jsx b/pages/logs/LogContents.jsx new file mode 100644 index 000000000..7fa3def65 --- /dev/null +++ b/pages/logs/LogContents.jsx @@ -0,0 +1,31 @@ +import React, { PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { Panel, Row, Col } from 'react-bootstrap'; + +class LogContents extends React.Component { + static propTypes = { + lines: PropTypes.array, + }; + + render() { + const { lines } = this.props; + return ( + + + {lines.join('\n')} + + + ); + } +} + +function mapStateToProps(state) { + const { contents } = state.logs; + const lines = contents.lines || []; + + return { + lines, + }; +} + +export default connect(mapStateToProps)(LogContents); diff --git a/pages/logs/LogSettings.jsx b/pages/logs/LogSettings.jsx new file mode 100644 index 000000000..2a67d1f90 --- /dev/null +++ b/pages/logs/LogSettings.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Panel, Checkbox } from 'react-bootstrap'; + +class LogSettings extends React.Component { + + render() { + return ( + + Errors + Warnings + + ); + } +} + +export default LogSettings; diff --git a/pages/logs/index.jsx b/pages/logs/index.jsx new file mode 100644 index 000000000..3cf622a07 --- /dev/null +++ b/pages/logs/index.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { Panel } from 'react-bootstrap'; +import history from '../../core/history'; +import store from '../../core/store'; +import { getDeltaAsync } from '../../core/actions/logs/Delta'; +import { setContents } from '../../core/actions/logs/Contents'; +import Layout from '../../components/Layout/Layout'; +import InfoPanel from '../../components/Panels/InfoPanel'; +import Overview from '../main/Overview'; +import LogSettings from './LogSettings'; +import LogContents from './LogContents'; + +class LogsPage extends React.Component { + componentDidMount() { + // eslint-disable-next-line no-undef + document.title = `JMM Server Web UI ${__VERSION__}`; + + const state = store.getState(); + if (state.apiSession.apikey === '') { + history.push({ + pathname: '/', + }); + return; + } + + // Fetch current log + getDeltaAsync().then(() => { + const newState = store.getState(); + store.dispatch(setContents(newState.logs.delta.items)); + }); + } + + render() { + return ( + +

    +
    + +
    + + Here be logs! + +
    + + +
    +
    + + ); + } +} + +export default LogsPage; diff --git a/pages/main/Commands.jsx b/pages/main/Commands.jsx index eb434a2ab..8b4a113c0 100644 --- a/pages/main/Commands.jsx +++ b/pages/main/Commands.jsx @@ -10,7 +10,6 @@ class Commands extends React.Component { className: PropTypes.string, isFetching: PropTypes.bool, lastUpdated: PropTypes.number, - autoUpdate: PropTypes.bool, items: PropTypes.object, }; @@ -38,7 +37,7 @@ class Commands extends React.Component { > - {commands} + {commands}
    diff --git a/pages/main/ImportFolders.jsx b/pages/main/ImportFolders.jsx index 56dc05ba9..414a40a8d 100644 --- a/pages/main/ImportFolders.jsx +++ b/pages/main/ImportFolders.jsx @@ -14,7 +14,7 @@ class ImportFolders extends React.Component { isFetching: PropTypes.bool, importModal: PropTypes.object, lastUpdated: PropTypes.number, - items: PropTypes.object, + items: PropTypes.array, description: PropTypes.string, importFolders: PropTypes.object, }; @@ -29,8 +29,8 @@ class ImportFolders extends React.Component { const folders = []; let i = 0; forEach(items, (item) => { - i++; - folders.push(); + i += 1; + folders.push(); }); return ( @@ -45,7 +45,7 @@ class ImportFolders extends React.Component { > - {folders} + {folders}
    diff --git a/pages/main/News.jsx b/pages/main/News.jsx index 0edeb8257..62ecfc740 100644 --- a/pages/main/News.jsx +++ b/pages/main/News.jsx @@ -17,7 +17,7 @@ class News extends React.Component { const news = []; let i = 0; forEach(items, (item) => { - i++; + i += 1; news.push(); }); @@ -31,7 +31,7 @@ class News extends React.Component { > - {news} + {news}
    diff --git a/pages/main/QuickActions.jsx b/pages/main/QuickActions.jsx index 1d9fb8386..d09c06fd8 100644 --- a/pages/main/QuickActions.jsx +++ b/pages/main/QuickActions.jsx @@ -27,7 +27,7 @@ class QuickActions extends React.Component { > - {actions} + {actions}
    diff --git a/pages/main/RecentFiles.jsx b/pages/main/RecentFiles.jsx index 139c8e4e8..f3950101e 100644 --- a/pages/main/RecentFiles.jsx +++ b/pages/main/RecentFiles.jsx @@ -9,12 +9,7 @@ class RecentFiles extends React.Component { className: PropTypes.string, isFetching: PropTypes.bool, lastUpdated: PropTypes.number, - items: PropTypes.arrayOf( - PropTypes.shape({ - path: PropTypes.string, - success: PropTypes.bool, - }) - ), + items: PropTypes.array, }; render() { @@ -22,8 +17,8 @@ class RecentFiles extends React.Component { const files = []; let i = 0; forEach(items, (item) => { - i++; - files.push(); + i += 1; + files.push(); }); return (
    @@ -35,7 +30,7 @@ class RecentFiles extends React.Component { > - {files} + {files}
    diff --git a/pages/settings/StyleSettings.jsx b/pages/settings/StyleSettings.jsx index a7b82b2a4..7fc6d219a 100644 --- a/pages/settings/StyleSettings.jsx +++ b/pages/settings/StyleSettings.jsx @@ -6,8 +6,6 @@ import FixedPanel from '../../components/Panels/FixedPanel'; class StyleSettings extends React.Component { static propTypes = { className: PropTypes.string, - isFetching: PropTypes.bool, - lastUpdated: PropTypes.number, theme: PropTypes.string, notifications: PropTypes.bool, }; diff --git a/pages/settings/index.jsx b/pages/settings/index.jsx index d1dc333c0..9949ed286 100644 --- a/pages/settings/index.jsx +++ b/pages/settings/index.jsx @@ -19,6 +19,7 @@ class SettingsPage extends React.Component { history.push({ pathname: '/', }); + return; } getLog(); diff --git a/routes.json b/routes.json index 968f3ad5e..cb10b8c5b 100644 --- a/routes.json +++ b/routes.json @@ -35,5 +35,9 @@ { "path": "/settings", "page": "./pages/settings" + }, + { + "path": "/logs", + "page": "./pages/logs" } ] diff --git a/utils/routes-loader.js b/utils/routes-loader.js index 31d4b2dcb..f7f3efbde 100644 --- a/utils/routes-loader.js +++ b/utils/routes-loader.js @@ -1,3 +1,4 @@ +/* eslint-disable */ const toRegExp = require('path-to-regexp'); function escape(text) {