Skip to content

Commit

Permalink
Fix updater (#128)
Browse files Browse the repository at this point in the history
* Fix updater

* More typechecks

* More tests, refactor polling to use sagas
  • Loading branch information
hidden4003 authored Mar 21, 2018
1 parent 92fdc1f commit 23c37da
Show file tree
Hide file tree
Showing 29 changed files with 580 additions and 141 deletions.
9 changes: 8 additions & 1 deletion .flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,11 @@ flow-typed/es6-promise_v4.2.4.js
flow-typed/promise.js

[options]
experimental.const_params=true
experimental.const_params=true
module.file_ext=.js
module.file_ext=.jsx
module.file_ext=.json
module.file_ext=.css
module.file_ext=.scss
module.name_mapper.extension='css' -> 'empty/object'
module.name_mapper.extension='scss' -> 'empty/object'
2 changes: 1 addition & 1 deletion flow-typed/react-bootstrap_v0.32.x.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ declare module "react-bootstrap" {
componentClass?: ElementType,
href?: string,
type?: 'button' | 'reset' | 'submit',
bsStyle?: 'default' | 'primary' | 'link' | 'danger' | 'success',
bsStyle?: 'default' | 'primary' | 'link' | 'danger' | 'success' | 'info',
bsSize?: BsStyle,
bsClass?: string,
}> {}
Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jmmserver-webui",
"version": "0.3.4-dev3",
"version": "0.3.4-dev4",
"private": true,
"engines": {
"node": ">=6",
Expand Down Expand Up @@ -69,6 +69,8 @@
"cross-env": "^5.1.1",
"css-loader": "^0.28.9",
"cssnano": "^3.10.0",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"eslint": "^4.10.0",
"eslint-config-airbnb": "^16.1.0",
"eslint-plugin-import": "^2.8.0",
Expand All @@ -89,7 +91,9 @@
"postcss-import": "^11.1.0",
"postcss-loader": "^2.0.8",
"postcss-url": "^7.1.2",
"react-addons-test-utils": "^15.6.2",
"react-hot-loader": "^3.1.1",
"redux-mock-store": "^1.5.1",
"redux-saga-testing": "^1.0.5",
"resolve-url-loader": "^2.2.1",
"sass-loader": "^6.0.6",
Expand Down Expand Up @@ -186,7 +190,7 @@
"build:version": "cross-env NODE_ENV=production node run version",
"start": "cross-env NODE_ENV=development node run",
"test": "ava",
"test:coverage": "nyc ava",
"test:coverage": "nyc --extension .jsx ava",
"report": "nyc report --reporter=html"
}
}
14 changes: 12 additions & 2 deletions src/components/AlertContainer.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
// @flow
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { forEach } from 'lodash';
import s from './AlertContainer.css';
import Notification from './Notification';

class AlertContainer extends React.Component {
type AlertType = {
type: 'success' | 'error',
text: string,
}

type Props = {
globalAlert: Array<AlertType>
}

class AlertContainer extends React.Component<Props> {
static propTypes = {
globalAlert: PropTypes.array.isRequired,
};
Expand Down Expand Up @@ -34,4 +44,4 @@ function mapStateToProps(state) {
};
}

export default connect(mapStateToProps)(AlertContainer);
export default connect(mapStateToProps, () => ({}))(AlertContainer);
18 changes: 12 additions & 6 deletions src/components/Buttons/AutoRefreshSwitch.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@ import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import cx from 'classnames';
import { setAutoupdate } from '../../core/legacy-actions';
import Events from '../../core/events';
import type { State } from '../../core/store';

type Props = {
autoUpdate: boolean,
setAutoUpdate: (value: boolean) => void
startPolling: () => void,
stopPolling: () => void,
};

class AutoRefreshSwitch extends React.Component<Props> {
export class AutoRefreshSwitch extends React.Component<Props> {
static propTypes = {
autoUpdate: PropTypes.bool.isRequired,
startPolling: PropTypes.func.isRequired,
stopPolling: PropTypes.func.isRequired,
};

handleClick = () => {
this.props.setAutoUpdate(!this.props.autoUpdate);
const { autoUpdate, startPolling, stopPolling } = this.props;
if (autoUpdate) { stopPolling(); } else { startPolling(); }
};

render() {
Expand All @@ -31,7 +36,7 @@ class AutoRefreshSwitch extends React.Component<Props> {
}
}

function mapStateToProps(state) {
function mapStateToProps(state: State) {
const { autoUpdate } = state;

return {
Expand All @@ -41,7 +46,8 @@ function mapStateToProps(state) {

function mapDispatchToProps(dispatch) {
return {
setAutoUpdate: value => dispatch(setAutoupdate(value)),
startPolling: () => dispatch({ type: Events.START_API_POLLING, payload: { type: 'auto-refresh' } }),
stopPolling: () => dispatch({ type: Events.STOP_API_POLLING, payload: { type: 'auto-refresh' } }),
};
}

Expand Down
4 changes: 2 additions & 2 deletions src/components/Layout/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Logo from './Logo';
import GeneralQueue from './GeneralQueue';
import HasherQueue from './HasherQueue';
import ImageQueue from './ImageQueue';
import AutoRefreshSwitch from '../Buttons/AutoRefreshSwitch';
import RefreshSwitch from '../Buttons/AutoRefreshSwitch';
import SidebarToggle from '../Buttons/SidebarToggle';
import UpdateButton from '../Buttons/UpdateButton';
import Notifications from './Notifications';
Expand All @@ -27,7 +27,7 @@ export default class Header extends React.Component<{}> {
<div className="nav notifications pull-right">
<ul className="nav">
<Notifications />
<AutoRefreshSwitch />
<RefreshSwitch />
</ul>
</div>
<UserDropdown />
Expand Down
7 changes: 4 additions & 3 deletions src/core/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import 'isomorphic-fetch';
import { createAction } from 'redux-actions';

export const STATUS_INVALIDATE = 'STATUS_INVALIDATE';
export const STATUS_REQUEST = 'STATUS_REQUEST';
export const STATUS_RECEIVE = 'STATUS_RECEIVE';
export type Action = {
type: string,
payload: any
}

/* Sync actions */
export const API_SESSION = 'API_SESSION';
Expand Down
1 change: 1 addition & 0 deletions src/core/actions/firstrun.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @flow
import { createAction } from 'redux-actions';

export const FIRSTRUN_STATUS = 'FIRSTRUN_STATUS';
Expand Down
7 changes: 3 additions & 4 deletions src/core/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'isomorphic-fetch';
import Promise from 'es6-promise';
import store from './store';
import Events from './events';
import { setAutoupdate } from './legacy-actions';

export type ApiResponseSuccessType = { data: any }
export type ApiResponseErrorType = { error: boolean, code?: number, message: string }
Expand Down Expand Up @@ -42,7 +41,7 @@ function apiCall(apiAction, apiParams: {} | string, type = 'GET') {
if (response.status === 401) {
// FIXME: make a better fix
store.dispatch({ type: Events.LOGOUT, payload: null });
setAutoupdate(false);
store.dispatch({ type: Events.STOP_API_POLLING, payload: { type: 'auto-refresh' } });
}
return Promise.reject(`Network error: ${apiAction} ${response.status}: ${response.statusText}`);
}
Expand Down Expand Up @@ -249,8 +248,8 @@ function getVersion() {
return jsonApiResponse('/version', '');
}

function getWebuiUpdate() {
return jsonApiResponse('/webui/update', '');
function getWebuiUpdate(channel: string) {
return jsonApiResponse('/webui/update', channel);
}

function getSerieInfobyfolder(data: string) {
Expand Down
41 changes: 0 additions & 41 deletions src/core/legacy-actions.js

This file was deleted.

1 change: 1 addition & 0 deletions src/core/reducers/firstrun.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @flow
import { combineReducers } from 'redux';
import { handleAction } from 'redux-actions';
import {
Expand Down
10 changes: 0 additions & 10 deletions src/core/reducers/logs/Delta.js

This file was deleted.

5 changes: 4 additions & 1 deletion src/core/sagas/AlertScheduler.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
// @flow
import { delay } from 'redux-saga';
import type { Saga } from 'redux-saga';
import { put, call } from 'redux-saga/effects';
import { without } from 'lodash/array';
import { GLOBAL_ALERT, SHOW_GLOBAL_ALERT } from '../actions';
import type { Action } from '../actions';

const maxAlerts = 2;
const alertDisplayTime = 3000;
let activeAlerts = [];

export default function* alertScheduler(action) {
export default function* alertScheduler(action: Action): Saga<void> {
const alert = action.payload;
if (activeAlerts.length < maxAlerts) {
activeAlerts = [...activeAlerts, alert];
Expand Down
5 changes: 4 additions & 1 deletion src/core/sagas/QueueGlobalAlert.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// @flow
import { put } from 'redux-saga/effects';
import type { Saga } from 'redux-saga';
import type { Action } from '../actions';

import { SHOW_GLOBAL_ALERT } from '../actions';

export default function* queueGlobalAlert(action) {
export default function* queueGlobalAlert(action: Action): Saga<void> {
yield put({ type: SHOW_GLOBAL_ALERT, payload: action.payload });
}
38 changes: 35 additions & 3 deletions src/core/sagas/apiPollingDriver.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
// @flow
import { delay } from 'redux-saga';
import { take, cancel, fork, call, put } from 'redux-saga/effects';
import type { Saga } from 'redux-saga';
import { take, cancel, fork, call, put, select, cancelled } from 'redux-saga/effects';
import Api from '../api';
import Events from '../events';
import { QUEUE_GLOBAL_ALERT } from '../actions';
import { QUEUE_GLOBAL_ALERT, SET_AUTOUPDATE } from '../actions';
import { getStatus } from '../actions/firstrun';
import type { Action } from '../actions';
import { getDelta } from '../actions/logs/Delta';


function* pollServerStatus() {
while (true) {
Expand All @@ -17,11 +22,38 @@ function* pollServerStatus() {
}
}

function* pollAutoRefresh() {
try {
yield put({ type: SET_AUTOUPDATE, payload: true });
while (true) {
const location = yield select(state => state.router.location.pathname);

if (location === '/dashboard') {
yield put({ type: Events.DASHBOARD_QUEUE_STATUS, payload: null });
yield put({ type: Events.DASHBOARD_RECENT_FILES, payload: null });
} else if (location === '/logs') {
const payload = yield select(state => ({
delta: state.settings.other.logDelta,
position: state.logs.contents.position,
}));
yield put(getDelta(payload));
}

yield call(delay, 4000);
}
} finally {
if (yield cancelled()) {
yield put({ type: SET_AUTOUPDATE, payload: false });
}
}
}

const typeMap = {
'server-status': pollServerStatus,
'auto-refresh': pollAutoRefresh,
};

export default function* apiPollingDriver(action) {
export default function* apiPollingDriver(action: Action): Saga<void> {
const { type } = action.payload;

if (typeof typeMap[type] !== 'function') {
Expand Down
3 changes: 1 addition & 2 deletions src/core/sagas/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
} from '../actions';
import { SET_THEME, SET_NOTIFICATIONS } from '../actions/settings/UI';
import { SET_LOG_DELTA, SET_UPDATE_CHANNEL } from '../actions/settings/Other';
import { setAutoupdate } from '../legacy-actions';
import Events from '../events';

function* getSettings(): Saga<void> {
Expand Down Expand Up @@ -125,7 +124,7 @@ function* eventDashboardLoad(): Saga<void> {

yield put({ type: JMM_NEWS, payload: resultJson.data });
yield put({ type: Events.CHECK_UPDATES });
yield put(setAutoupdate(true));
yield put({ type: Events.START_API_POLLING, payload: { type: 'auto-refresh' } });
}

export default {
Expand Down
3 changes: 2 additions & 1 deletion src/core/sagas/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,8 @@ function* serverVersion(): Saga<void> {

function* downloadUpdates(): Saga<void> {
yield dispatchAction(Events.START_FETCHING, 'downloadUpdates');
const resultJson = yield call(Api.getWebuiUpdate);
const channel = yield select(state => state.settings.other.updateChannel);
const resultJson = yield call(Api.getWebuiUpdate, channel);
yield dispatchAction(Events.STOP_FETCHING, 'downloadUpdates');
if (resultJson.error) {
yield dispatchAction(WEBUI_VERSION_UPDATE, { error: resultJson.message });
Expand Down
6 changes: 1 addition & 5 deletions src/core/store.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ import rootSaga from './sagas';
import history from './history';

import type { Reducers } from './reducers';

type Action = {
type: string,
payload: any
}
import type { Action } from './actions';

type $ExtractFunctionReturn = <V>(v: (...args: any) => V) => V;
export type State = $ObjMap<Reducers, $ExtractFunctionReturn>;
Expand Down
Loading

0 comments on commit 23c37da

Please sign in to comment.