Merge remote-tracking branch 'jason/websocket'

This commit is contained in:
Maximilian Hils
2016-06-23 23:38:19 -07:00
6 changed files with 184 additions and 102 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +1,15 @@
import React from "react"
import React from 'react'
import { render } from 'react-dom'
import { applyMiddleware, createStore } from 'redux'
import { Provider } from 'react-redux'
import createLogger from 'redux-logger'
import thunkMiddleware from 'redux-thunk'
import { Route, Router as ReactRouter, hashHistory, Redirect } from "react-router"
import { Route, Router as ReactRouter, hashHistory, Redirect } from 'react-router'
import Connection from "./connection"
import ProxyApp from "./components/ProxyApp"
import ProxyApp from './components/ProxyApp'
import MainView from './components/MainView'
import rootReducer from './ducks/index'
import { addLogEntry } from "./ducks/eventLog"
import { addLogEntry } from './ducks/eventLog'
// logger must be last
const store = createStore(
@@ -18,14 +17,13 @@ const store = createStore(
applyMiddleware(thunkMiddleware, createLogger())
)
// @todo move to ProxyApp
window.addEventListener('error', msg => {
store.dispatch(addLogEntry(msg))
})
// @todo remove this
document.addEventListener('DOMContentLoaded', () => {
window.ws = new Connection("/updates", store.dispatch)
render(
<Provider store={store}>
<ReactRouter history={hashHistory}>

View File

@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'
import _ from 'lodash'
import { connect } from 'react-redux'
import { init as appInit, destruct as appDestruct } from '../ducks/app'
import Header from './Header'
import EventLog from './EventLog'
import Footer from './Footer'
@@ -26,27 +27,8 @@ class ProxyAppMain extends Component {
this.updateLocation = this.updateLocation.bind(this)
}
/**
* @todo move to actions
*/
updateLocation(pathname, queryUpdate) {
if (pathname === undefined) {
pathname = this.props.location.pathname
}
const query = this.props.location.query
for (const key of Object.keys(queryUpdate || {})) {
query[key] = queryUpdate[key] || undefined
}
this.context.router.replace({ pathname, query })
}
/**
* @todo pass in with props
*/
getQuery() {
// For whatever reason, react-router always returns the same object, which makes comparing
// the current props with nextProps impossible. As a workaround, we just clone the query object.
return _.clone(this.props.location.query)
componentWillMount() {
this.props.appInit()
}
/**
@@ -56,6 +38,10 @@ class ProxyAppMain extends Component {
this.focus()
}
componentWillUnmount() {
this.props.appDestruct()
}
/**
* @todo use props
*/
@@ -110,6 +96,29 @@ class ProxyAppMain extends Component {
e.preventDefault()
}
/**
* @todo move to actions
*/
updateLocation(pathname, queryUpdate) {
if (pathname === undefined) {
pathname = this.props.location.pathname
}
const query = this.props.location.query
for (const key of Object.keys(queryUpdate || {})) {
query[key] = queryUpdate[key] || undefined
}
this.context.router.replace({ pathname, query })
}
/**
* @todo pass in with props
*/
getQuery() {
// For whatever reason, react-router always returns the same object, which makes comparing
// the current props with nextProps impossible. As a workaround, we just clone the query object.
return _.clone(this.props.location.query)
}
render() {
const { showEventLog, location, children } = this.props
const query = this.getQuery()
@@ -132,5 +141,10 @@ class ProxyAppMain extends Component {
export default connect(
state => ({
showEventLog: state.eventLog.visible,
})
settings: state.settings.settings,
}),
{
appInit,
appDestruct,
}
)(ProxyAppMain)

View File

@@ -1,49 +0,0 @@
import {ConnectionActions} from "./actions.js";
import {AppDispatcher} from "./dispatcher.js";
import * as webSocketActions from "./ducks/websocket"
import * as eventLogActions from "./ducks/eventLog"
import * as flowActions from "./ducks/flows"
import * as settingsActions from './ducks/settings'
export default function Connection(url, dispatch) {
if (url[0] === "/") {
url = location.origin.replace("http", "ws") + url;
}
var ws = new WebSocket(url);
ws.onopen = function () {
dispatch(webSocketActions.connected())
dispatch(settingsActions.fetchSettings())
dispatch(flowActions.fetchFlows())
// workaround to make sure that our state is already available.
.then(() => {
console.log("flows are loaded now")
ConnectionActions.open()
})
dispatch(eventLogActions.fetchLogEntries())
};
ws.onmessage = function (m) {
var message = JSON.parse(m.data);
AppDispatcher.dispatchServerAction(message);
switch (message.type) {
case eventLogActions.UPDATE_LOG:
return dispatch(eventLogActions.updateLogEntries(message))
case flowActions.UPDATE_FLOWS:
return dispatch(flowActions.updateFlows(message))
case settingsActions.UPDATE_SETTINGS:
return dispatch(settingsActions.handleWsMsg(message))
default:
console.warn("unknown message", message)
}
};
ws.onerror = function () {
ConnectionActions.error();
dispatch(eventLogActions.addLogEntry("WebSocket connection error."));
};
ws.onclose = function () {
ConnectionActions.close();
dispatch(eventLogActions.addLogEntry("WebSocket connection closed."));
dispatch(webSocketActions.disconnected());
};
return ws;
}

27
web/src/js/ducks/app.js Normal file
View File

@@ -0,0 +1,27 @@
import { connect as wsConnect, disconnect as wsDisconnect } from './websocket'
export const INIT = 'APP_INIT'
const defaultState = {}
export function reduce(state = defaultState, action) {
switch (action.type) {
default:
return state
}
}
export function init() {
return dispatch => {
dispatch(wsConnect())
dispatch({ type: INIT })
}
}
export function destruct() {
return dispatch => {
dispatch(wsDisconnect())
dispatch({ type: DESTRUCT })
}
}

View File

@@ -1,30 +1,113 @@
const CONNECTED = 'WEBSOCKET_CONNECTED'
const DISCONNECTED = 'WEBSOCKET_DISCONNECTED'
import { ConnectionActions } from '../actions.js'
import { AppDispatcher } from '../dispatcher.js'
import * as eventLogActions from './eventLog'
import * as flowsActions from './flows'
import * as settingsActions from './settings'
export const SYM_SOCKET = Symbol('WEBSOCKET_SYM_SOCKET')
const defaultState = {
connected: false,
/* we may want to have an error message attribute here at some point */
}
export default function reducer(state = defaultState, action) {
export const CONNECT = 'WEBSOCKET_CONNECT'
export const CONNECTED = 'WEBSOCKET_CONNECTED'
export const DISCONNECT = 'WEBSOCKET_DISCONNECT'
export const DISCONNECTED = 'WEBSOCKET_DISCONNECTED'
export const ERROR = 'WEBSOCKET_ERROR'
export const MESSAGE = 'WEBSOCKET_MESSAGE'
/* we may want to have an error message attribute here at some point */
const defaultState = { connected: false, socket: null }
export default function reduce(state = defaultState, action) {
switch (action.type) {
case CONNECT:
return { ...state, [SYM_SOCKET]: action.socket }
case CONNECTED:
return {
connected: true
}
return { ...state, connected: true }
case DISCONNECT:
return { ...state, connected: false }
case DISCONNECTED:
return {
connected: false
}
return { ...state, [SYM_SOCKET]: null, connected: false }
default:
return state
}
}
export function connect() {
return dispatch => {
const socket = new WebSocket(location.origin.replace('http', 'ws') + '/updates')
export function connected() {
return {type: CONNECTED}
// @todo remove this
window.ws = socket
socket.addEventListener('open', () => dispatch(onConnect()))
socket.addEventListener('close', () => dispatch(onDisconnect()))
socket.addEventListener('message', msg => dispatch(onMessage(msg)))
socket.addEventListener('error', error => dispatch(onError(error)))
dispatch({ type: CONNECT, socket })
return socket
}
}
export function disconnected() {
return {type: DISCONNECTED}
export function disconnect() {
return (dispatch, getState) => {
getState().settings[SYM_SOCKET].close()
dispatch({ type: DISCONNECT })
}
}
export function onConnect() {
// workaround to make sure that our state is already available.
return dispatch => {
dispatch({ type: CONNECTED })
dispatch(settingsActions.fetchSettings())
dispatch(flowsActions.fetchFlows()).then(() => ConnectionActions.open())
}
}
export function onMessage(msg) {
return dispatch => {
const data = JSON.parse(msg.data)
AppDispatcher.dispatchServerAction(data)
switch (data.type) {
case eventLogActions.UPDATE_LOG:
return dispatch(eventLogActions.updateLogEntries(data))
case flowsActions.UPDATE_FLOWS:
return dispatch(flowsActions.updateFlows(data))
case settingsActions.UPDATE_SETTINGS:
return dispatch(settingsActions.updateSettings(message))
default:
console.warn('unknown message', data)
}
dispatch({ type: MESSAGE, msg })
}
}
export function onDisconnect() {
return dispatch => {
ConnectionActions.close()
dispatch(eventLogActions.addLogEntry('WebSocket connection closed.'))
dispatch({ type: DISCONNECTED })
}
}
export function onError(error) {
// @todo let event log subscribe WebSocketActions.ERROR
return dispatch => {
ConnectionActions.error()
dispatch(eventLogActions.addLogEntry('WebSocket connection error.'))
dispatch({ type: ERROR, error })
}
}