clean up mitmweb

This commit is contained in:
Maximilian Hils
2016-10-31 05:30:32 -07:00
parent 62ca896492
commit 85476d9915
21 changed files with 2699 additions and 3066 deletions

View File

@@ -96,6 +96,12 @@ class BasicAuth:
class RequestHandler(BasicAuth, tornado.web.RequestHandler):
def write(self, chunk):
if isinstance(chunk, list):
chunk = tornado.escape.json_encode(chunk)
self.set_header("Content-Type", "application/json; charset=UTF-8")
super(RequestHandler, self).write(chunk)
def set_default_headers(self):
super().set_default_headers()
self.set_header("Server", version.MITMPROXY)
@@ -184,10 +190,7 @@ class ClientConnection(WebSocketEventBroadcaster):
class Flows(RequestHandler):
def get(self):
self.write(dict(
data=[convert_flow_to_json_dict(f) for f in self.view]
))
self.write([convert_flow_to_json_dict(f) for f in self.view])
class DumpFlows(RequestHandler):
def get(self):
@@ -355,9 +358,7 @@ class FlowContentView(RequestHandler):
class Events(RequestHandler):
def get(self):
self.write(dict(
data=list([])
))
self.write([]) # FIXME
class Settings(RequestHandler):
@@ -365,7 +366,6 @@ class Settings(RequestHandler):
def get(self):
self.write(dict(
data=dict(
version=version.VERSION,
mode=str(self.master.options.mode),
intercept=self.master.options.intercept,
@@ -377,10 +377,10 @@ class Settings(RequestHandler):
anticomp=self.master.options.anticomp,
stickyauth=self.master.options.stickyauth,
stickycookie=self.master.options.stickycookie,
stream= self.master.options.stream_large_bodies,
contentViews= [v.name.replace(' ', '_') for v in contentviews.views]
stream=self.master.options.stream_large_bodies,
contentViews=[v.name.replace(' ', '_') for v in contentviews.views]
)
))
)
def put(self):
update = {}
@@ -419,7 +419,7 @@ class Settings(RequestHandler):
print("Warning: Unknown setting {}: {}".format(k, v))
ClientConnection.broadcast(
type="UPDATE_SETTINGS",
resource="settings",
cmd="update",
data=update
)

View File

@@ -29,7 +29,7 @@ class _WebState():
}
self.events.append(entry)
app.ClientConnection.broadcast(
type="UPDATE_EVENTLOG",
resource="events",
cmd="add",
data=entry
)
@@ -38,9 +38,8 @@ class _WebState():
super().clear()
self.events.clear()
app.ClientConnection.broadcast(
type="UPDATE_EVENTLOG",
cmd="reset",
data=[]
resource="events",
cmd="reset"
)
@@ -113,28 +112,28 @@ class WebMaster(master.Master):
def _sig_add(self, view, flow):
app.ClientConnection.broadcast(
type="UPDATE_FLOWS",
resource="flows",
cmd="add",
data=app.convert_flow_to_json_dict(flow)
)
def _sig_update(self, view, flow):
app.ClientConnection.broadcast(
type="UPDATE_FLOWS",
resource="flows",
cmd="update",
data=app.convert_flow_to_json_dict(flow)
)
def _sig_remove(self, view, flow):
app.ClientConnection.broadcast(
type="UPDATE_FLOWS",
resource="flows",
cmd="remove",
data=dict(id=flow.id)
)
def _sig_refresh(self, view):
app.ClientConnection.broadcast(
type="UPDATE_FLOWS",
resource="flows",
cmd="reset"
)

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,6 @@
"bootstrap": "^3.3.6",
"classnames": "^2.2.5",
"flux": "^2.1.1",
"history": "^3.0.0",
"lodash": "^4.11.2",
"react": "^15.1.0",
"react-codemirror": "^0.2.6",

View File

@@ -1,44 +0,0 @@
import {AppDispatcher} from "./dispatcher.js";
export var ActionTypes = {
// Connection
CONNECTION_OPEN: "connection_open",
CONNECTION_CLOSE: "connection_close",
CONNECTION_ERROR: "connection_error",
// Stores
SETTINGS_STORE: "settings",
EVENT_STORE: "events",
FLOW_STORE: "flows"
};
export var StoreCmds = {
ADD: "add",
UPDATE: "update",
REMOVE: "remove",
RESET: "reset"
};
export var ConnectionActions = {
open: function () {
AppDispatcher.dispatchViewAction({
type: ActionTypes.CONNECTION_OPEN
});
},
close: function () {
AppDispatcher.dispatchViewAction({
type: ActionTypes.CONNECTION_CLOSE
});
},
error: function () {
AppDispatcher.dispatchViewAction({
type: ActionTypes.CONNECTION_ERROR
});
}
};
export var Query = {
SEARCH: "s",
HIGHLIGHT: "h",
SHOW_EVENTLOG: "e"
};

View File

@@ -7,6 +7,9 @@ import thunk from 'redux-thunk'
import ProxyApp from './components/ProxyApp'
import rootReducer from './ducks/index'
import { add as addLog } from './ducks/eventLog'
import useUrlState from './urlState'
import WebSocketBackend from './backends/websocket'
const middlewares = [thunk];
@@ -21,12 +24,13 @@ const store = createStore(
applyMiddleware(...middlewares)
)
// @todo move to ProxyApp
useUrlState(store)
window.backend = new WebSocketBackend(store)
window.addEventListener('error', msg => {
store.dispatch(addLog(msg))
})
// @todo remove this
document.addEventListener('DOMContentLoaded', () => {
render(
<Provider store={store}>

View File

@@ -0,0 +1,68 @@
import { fetchApi } from "../utils"
export const CMD_RESET = 'reset'
export default class WebsocketBackend {
constructor(store) {
this.activeFetches = {}
this.store = store
this.connect()
}
connect() {
this.socket = new WebSocket(location.origin.replace('http', 'ws') + '/updates')
this.socket.addEventListener('open', () => this.onOpen())
this.socket.addEventListener('close', () => this.onClose())
this.socket.addEventListener('message', msg => this.onMessage(JSON.parse(msg.data)))
this.socket.addEventListener('error', error => this.onError(error))
}
onOpen() {
this.fetchData("settings")
this.fetchData("flows")
this.fetchData("events")
}
fetchData(resource) {
let queue = []
this.activeFetches[resource] = queue
fetchApi(`/${resource}`)
.then(res => res.json())
.then(json => {
// Make sure that we are not superseded yet by the server sending a RESET.
if (this.activeFetches[resource] === queue)
this.receive(resource, json)
})
}
onMessage(msg) {
if (msg.cmd === CMD_RESET) {
return this.fetchData(msg.resource)
}
if (msg.resource in this.activeFetches) {
this.activeFetches[msg.resource].push(msg)
} else {
let type = `${msg.resource}_${msg.cmd}`.toUpperCase()
this.store.dispatch({ type, ...msg })
}
}
receive(resource, msg) {
let type = `${resource}_RECEIVE`.toUpperCase()
this.store.dispatch({ type, [resource]: msg })
let queue = this.activeFetches[resource]
delete this.activeFetches[resource]
queue.forEach(msg => this.onMessage(msg))
}
onClose() {
// FIXME
console.error("onClose", arguments)
}
onError() {
// FIXME
console.error("onError", arguments)
}
}

View File

@@ -1,13 +1,7 @@
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { createHashHistory, useQueries } from 'history'
import { init as appInit, destruct as appDestruct } from '../ducks/app'
import { onKeyDown } from '../ducks/ui/keyboard'
import { updateFilter, updateHighlight } from '../ducks/flowView'
import { selectTab } from '../ducks/ui/flow'
import { select as selectFlow } from '../ducks/flows'
import { Query } from '../actions'
import MainView from './MainView'
import Header from './Header'
import EventLog from './EventLog'
@@ -15,57 +9,14 @@ import Footer from './Footer'
class ProxyAppMain extends Component {
flushToStore(location) {
const components = location.pathname.split('/').filter(v => v)
const query = location.query || {}
if (components.length > 2) {
this.props.selectFlow(components[1])
this.props.selectTab(components[2])
} else {
this.props.selectFlow(null)
this.props.selectTab(null)
}
this.props.updateFilter(query[Query.SEARCH])
this.props.updateHighlight(query[Query.HIGHLIGHT])
}
flushToHistory(props) {
const query = { ...query }
if (props.filter) {
query[Query.SEARCH] = props.filter
}
if (props.highlight) {
query[Query.HIGHLIGHT] = props.highlight
}
if (props.selectedFlowId) {
this.history.push({ pathname: `/flows/${props.selectedFlowId}/${props.tab}`, query })
} else {
this.history.push({ pathname: '/flows', query })
}
}
componentWillMount() {
this.props.appInit()
this.history = useQueries(createHashHistory)()
this.unlisten = this.history.listen(location => this.flushToStore(location))
window.addEventListener('keydown', this.props.onKeyDown);
}
componentWillUnmount() {
this.props.appDestruct()
this.unlisten()
window.removeEventListener('keydown', this.props.onKeyDown);
}
componentWillReceiveProps(nextProps) {
this.flushToHistory(nextProps)
}
render() {
const { showEventLog, location, filter, highlight } = this.props
return (
@@ -84,18 +35,8 @@ class ProxyAppMain extends Component {
export default connect(
state => ({
showEventLog: state.eventLog.visible,
filter: state.flowView.filter,
highlight: state.flowView.highlight,
tab: state.ui.flow.tab,
selectedFlowId: state.flows.selected[0]
}),
{
appInit,
appDestruct,
onKeyDown,
updateFilter,
updateHighlight,
selectTab,
selectFlow
}
)(ProxyAppMain)

View File

@@ -1,18 +0,0 @@
import flux from "flux";
const PayloadSources = {
VIEW: "view",
SERVER: "server"
};
export var AppDispatcher = new flux.Dispatcher();
AppDispatcher.dispatchViewAction = function (action) {
action.source = PayloadSources.VIEW;
this.dispatch(action);
};
AppDispatcher.dispatchServerAction = function (action) {
action.source = PayloadSources.SERVER;
this.dispatch(action);
};

View File

@@ -1,27 +0,0 @@
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,17 +1,12 @@
import reduceList, * as listActions from './utils/list'
import reduceView, * as viewActions from './utils/view'
import * as websocketActions from './websocket'
import * as msgQueueActions from './msgQueue'
export const MSG_TYPE = 'UPDATE_EVENTLOG'
export const DATA_URL = '/events'
export const ADD = 'EVENTLOG_ADD'
export const RECEIVE = 'EVENTLOG_RECEIVE'
export const TOGGLE_VISIBILITY = 'EVENTLOG_TOGGLE_VISIBILITY'
export const TOGGLE_FILTER = 'EVENTLOG_TOGGLE_FILTER'
export const UNKNOWN_CMD = 'EVENTLOG_UNKNOWN_CMD'
export const FETCH_ERROR = 'EVENTLOG_FETCH_ERROR'
export const ADD = 'EVENTS_ADD'
export const RECEIVE = 'EVENTS_RECEIVE'
export const TOGGLE_VISIBILITY = 'EVENTS_TOGGLE_VISIBILITY'
export const TOGGLE_FILTER = 'EVENTS_TOGGLE_FILTER'
export const UNKNOWN_CMD = 'EVENTS_UNKNOWN_CMD'
export const FETCH_ERROR = 'EVENTS_FETCH_ERROR'
const defaultState = {
logId: 0,
@@ -54,8 +49,8 @@ export default function reduce(state = defaultState, action) {
case RECEIVE:
return {
...state,
list: reduceList(state.list, listActions.receive(action.list)),
view: reduceView(state.view, viewActions.receive(action.list, log => state.filters[log.level])),
list: reduceList(state.list, listActions.receive(action.events)),
view: reduceView(state.view, viewActions.receive(action.events, log => state.filters[log.level])),
}
default:
@@ -63,58 +58,14 @@ export default function reduce(state = defaultState, action) {
}
}
/**
* @public
*/
export function toggleFilter(filter) {
return { type: TOGGLE_FILTER, filter }
}
/**
* @public
*
* @todo move to ui?
*/
export function toggleVisibility() {
return { type: TOGGLE_VISIBILITY }
}
/**
* @public
*/
export function add(message, level = 'web') {
return { type: ADD, message, level }
}
/**
* This action creater takes all WebSocket events
*
* @public websocket
*/
export function handleWsMsg(msg) {
switch (msg.cmd) {
case websocketActions.CMD_ADD:
return add(msg.data.message, msg.data.level)
case websocketActions.CMD_RESET:
return fetchData()
default:
return { type: UNKNOWN_CMD, msg }
}
}
/**
* @public websocket
*/
export function fetchData() {
return msgQueueActions.fetchData(MSG_TYPE)
}
/**
* @public msgQueue
*/
export function receiveData(list) {
return { type: RECEIVE, list }
}

View File

@@ -121,6 +121,16 @@ export default function reduce(state = defaultState, action) {
}
case flowActions.REMOVE:
/* FIXME: Implement select switch on remove
return (dispatch, getState) => {
let currentIndex = getState().flowView.indexOf[getState().flows.selected[0]]
let maxIndex = getState().flowView.data.length - 1
let deleteLastEntry = maxIndex == 0
if (deleteLastEntry)
dispatch(select())
else
dispatch(selectRelative(currentIndex == maxIndex ? -1 : 1) )
*/
return {
...reduceView(
state,
@@ -135,7 +145,7 @@ export default function reduce(state = defaultState, action) {
...reduceView(
state,
viewActions.receive(
action.list,
action.flows,
makeFilter(state.filter),
makeSort(state.sort)
)
@@ -149,33 +159,20 @@ export default function reduce(state = defaultState, action) {
}
}
/**
* @public
*/
export function updateFilter(filter) {
return (dispatch, getState) => {
dispatch({ type: UPDATE_FILTER, filter, flows: getState().flows.data })
}
}
/**
* @public
*/
export function updateHighlight(highlight) {
return { type: UPDATE_HIGHLIGHT, highlight }
}
/**
* @public
*/
export function updateSort(column, desc) {
return { type: UPDATE_SORT, column, desc }
}
/**
* @public
*/
export function selectRelative(shift) {
return (dispatch, getState) => {
let currentSelectionIndex = getState().flowView.indexOf[getState().flows.selected[0]]

View File

@@ -2,12 +2,6 @@ import { fetchApi } from '../utils'
import reduceList, * as listActions from './utils/list'
import { selectRelative } from './flowView'
import * as msgQueueActions from './msgQueue'
import * as websocketActions from './websocket'
export const MSG_TYPE = 'UPDATE_FLOWS'
export const DATA_URL = '/flows'
export const ADD = 'FLOWS_ADD'
export const UPDATE = 'FLOWS_UPDATE'
export const REMOVE = 'FLOWS_REMOVE'
@@ -47,7 +41,7 @@ export default function reduce(state = defaultState, action) {
case RECEIVE:
return {
...state,
...reduceList(state, listActions.receive(action.list)),
...reduceList(state, listActions.receive(action.flows)),
}
case SELECT:
@@ -64,51 +58,30 @@ export default function reduce(state = defaultState, action) {
}
}
/**
* @public
*/
export function accept(flow) {
return dispatch => fetchApi(`/flows/${flow.id}/accept`, { method: 'POST' })
}
/**
* @public
*/
export function acceptAll() {
return dispatch => fetchApi('/flows/accept', { method: 'POST' })
}
/**
* @public
*/
export function remove(flow) {
return dispatch => fetchApi(`/flows/${flow.id}`, { method: 'DELETE' })
}
/**
* @public
*/
export function duplicate(flow) {
return dispatch => fetchApi(`/flows/${flow.id}/duplicate`, { method: 'POST' })
}
/**
* @public
*/
export function replay(flow) {
return dispatch => fetchApi(`/flows/${flow.id}/replay`, { method: 'POST' })
}
/**
* @public
*/
export function revert(flow) {
return dispatch => fetchApi(`/flows/${flow.id}/revert`, { method: 'POST' })
}
/**
* @public
*/
export function update(flow, data) {
return dispatch => fetchApi.put(`/flows/${flow.id}`, data)
}
@@ -121,24 +94,15 @@ export function uploadContent(flow, file, type) {
}
/**
* @public
*/
export function clear() {
return dispatch => fetchApi('/clear', { method: 'POST' })
}
/**
* @public
*/
export function download() {
window.location = '/flows/dump'
return { type: REQUEST_ACTION }
}
/**
* @public
*/
export function upload(file) {
const body = new FormData()
body.append('file', file)
@@ -152,73 +116,3 @@ export function select(id) {
flowIds: id ? [id] : []
}
}
/**
* This action creater takes all WebSocket events
*
* @public websocket
*/
export function handleWsMsg(msg) {
switch (msg.cmd) {
case websocketActions.CMD_ADD:
return addFlow(msg.data)
case websocketActions.CMD_UPDATE:
return updateFlow(msg.data)
case websocketActions.CMD_REMOVE:
return removeFlow(msg.data.id)
case websocketActions.CMD_RESET:
return fetchFlows()
default:
return { type: UNKNOWN_CMD, msg }
}
}
/**
* @public websocket
*/
export function fetchFlows() {
return msgQueueActions.fetchData(MSG_TYPE)
}
/**
* @public msgQueue
*/
export function receiveData(list) {
return { type: RECEIVE, list }
}
/**
* @private
*/
export function addFlow(item) {
return { type: ADD, item }
}
/**
* @private
*/
export function updateFlow(item) {
return { type: UPDATE, item }
}
/**
* @private
*/
export function removeFlow(id) {
return (dispatch, getState) => {
let currentIndex = getState().flowView.indexOf[getState().flows.selected[0]]
let maxIndex = getState().flowView.data.length - 1
let deleteLastEntry = maxIndex == 0
if (deleteLastEntry)
dispatch(select())
else
dispatch(selectRelative(currentIndex == maxIndex ? -1 : 1) )
dispatch({ type: REMOVE, id })
}
}

View File

@@ -1,113 +0,0 @@
import { fetchApi } from '../utils'
import * as websocketActions from './websocket'
import * as eventLogActions from './eventLog'
import * as flowsActions from './flows'
import * as settingsActions from './settings'
export const INIT = 'MSG_QUEUE_INIT'
export const ENQUEUE = 'MSG_QUEUE_ENQUEUE'
export const CLEAR = 'MSG_QUEUE_CLEAR'
export const FETCH_ERROR = 'MSG_QUEUE_FETCH_ERROR'
const handlers = {
[eventLogActions.MSG_TYPE] : eventLogActions,
[flowsActions.MSG_TYPE] : flowsActions,
[settingsActions.MSG_TYPE] : settingsActions,
}
const defaultState = {}
export default function reduce(state = defaultState, action) {
switch (action.type) {
case INIT:
return {
...state,
[action.queue]: [],
}
case ENQUEUE:
return {
...state,
[action.queue]: [...state[action.queue], action.msg],
}
case CLEAR:
return {
...state,
[action.queue]: null,
}
default:
return state
}
}
/**
* @public websocket
*/
export function handleWsMsg(msg) {
return (dispatch, getState) => {
const handler = handlers[msg.type]
if (msg.cmd === websocketActions.CMD_RESET) {
return dispatch(fetchData(handler.MSG_TYPE))
}
if (getState().msgQueue[handler.MSG_TYPE]) {
return dispatch({ type: ENQUEUE, queue: handler.MSG_TYPE, msg })
}
return dispatch(handler.handleWsMsg(msg))
}
}
/**
* @public
*/
export function fetchData(type) {
return dispatch => {
const handler = handlers[type]
dispatch(init(handler.MSG_TYPE))
fetchApi(handler.DATA_URL)
.then(res => res.json())
.then(json => dispatch(receive(type, json)))
.catch(error => dispatch(fetchError(type, error)))
}
}
/**
* @private
*/
export function receive(type, res) {
return (dispatch, getState) => {
const handler = handlers[type]
const queue = getState().msgQueue[handler.MSG_TYPE] || []
dispatch(clear(handler.MSG_TYPE))
dispatch(handler.receiveData(res.data))
for (const msg of queue) {
dispatch(handler.handleWsMsg(msg))
}
}
}
/**
* @private
*/
export function init(queue) {
return { type: INIT, queue }
}
/**
* @private
*/
export function clear(queue) {
return { type: CLEAR, queue }
}
/**
* @private
*/
export function fetchError(type, error) {
return { type: FETCH_ERROR, type, error }
}

View File

@@ -1,12 +1,7 @@
import { fetchApi } from '../utils'
import * as websocketActions from './websocket'
import * as msgQueueActions from './msgQueue'
export const MSG_TYPE = 'UPDATE_SETTINGS'
export const DATA_URL = '/settings'
export const RECEIVE = 'RECEIVE'
export const UPDATE = 'UPDATE'
export const RECEIVE = 'SETTINGS_RECEIVE'
export const UPDATE = 'SETTINGS_UPDATE'
export const REQUEST_UPDATE = 'REQUEST_UPDATE'
export const UNKNOWN_CMD = 'SETTINGS_UNKNOWN_CMD'
@@ -23,7 +18,7 @@ export default function reducer(state = defaultState, action) {
case UPDATE:
return {
...state,
...action.settings,
...action.data,
}
default:
@@ -31,46 +26,7 @@ export default function reducer(state = defaultState, action) {
}
}
/**
* @public msgQueue
*/
export function handleWsMsg(msg) {
switch (msg.cmd) {
case websocketActions.CMD_UPDATE:
return updateSettings(msg.data)
default:
console.error('unknown settings update', msg)
return { type: UNKNOWN_CMD, msg }
}
}
/**
* @public
*/
export function update(settings) {
fetchApi.put('/settings', settings)
return { type: REQUEST_UPDATE }
}
/**
* @public websocket
*/
export function fetchData() {
return msgQueueActions.fetchData(MSG_TYPE)
}
/**
* @public msgQueue
*/
export function receiveData(settings) {
return { type: RECEIVE, settings }
}
/**
* @private
*/
export function updateSettings(settings) {
return { type: UPDATE, settings }
}

View File

@@ -2,6 +2,7 @@ import { combineReducers } from 'redux'
import flow from './flow'
import header from './header'
// TODO: Just move ducks/ui/* into ducks/?
export default combineReducers({
flow,
header,

View File

@@ -76,30 +76,18 @@ export default function reduce(state = defaultState, action) {
}
}
/**
* @public
*/
export function add(item) {
return { type: ADD, item }
}
/**
* @public
*/
export function update(item) {
return { type: UPDATE, item }
}
/**
* @public
*/
export function remove(id) {
return { type: REMOVE, id }
}
/**
* @public
*/
export function receive(list) {
return { type: RECEIVE, list }
}

View File

@@ -1,93 +0,0 @@
import { ConnectionActions } from '../actions.js'
import { AppDispatcher } from '../dispatcher.js'
import * as msgQueueActions from './msgQueue'
import * as eventLogActions from './eventLog'
import * as flowsActions from './flows'
import * as settingsActions from './settings'
export const CMD_ADD = 'add'
export const CMD_UPDATE = 'update'
export const CMD_REMOVE = 'remove'
export const CMD_RESET = 'reset'
export const SYM_SOCKET = Symbol('WEBSOCKET_SYM_SOCKET')
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 { ...state, connected: true }
case DISCONNECT:
return { ...state, connected: false }
case DISCONNECTED:
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')
socket.addEventListener('open', () => dispatch(onConnect()))
socket.addEventListener('close', () => dispatch(onDisconnect()))
socket.addEventListener('message', msg => dispatch(onMessage(JSON.parse(msg.data))))
socket.addEventListener('error', error => dispatch(onError(error)))
dispatch({ type: CONNECT, socket })
}
}
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.fetchData())
dispatch(flowsActions.fetchFlows())
dispatch(eventLogActions.fetchData())
}
}
export function onMessage(msg) {
return msgQueueActions.handleWsMsg(msg)
}
export function onDisconnect() {
return dispatch => {
dispatch(eventLogActions.add('WebSocket connection closed.'))
dispatch({ type: DISCONNECTED })
}
}
export function onError(error) {
// @todo let event log subscribe WebSocketActions.ERROR
return dispatch => {
dispatch(eventLogActions.add('WebSocket connection error.'))
dispatch({ type: ERROR, error })
}
}

78
web/src/js/urlState.js Normal file
View File

@@ -0,0 +1,78 @@
import { select } from "./ducks/flows"
import { selectTab } from "./ducks/ui/flow"
import { updateFilter, updateHighlight } from "./ducks/flowView"
import { toggleVisibility } from "./ducks/eventLog"
const Query = {
SEARCH: "s",
HIGHLIGHT: "h",
SHOW_EVENTLOG: "e"
};
function updateStoreFromUrl(store) {
const [path, query] = window.location.hash.substr(1).split("?", 2)
const path_components = path.substr(1).split("/")
if (path_components[0] === "flows") {
if (path_components.length == 3) {
const [flowId, tab] = path_components.slice(1)
store.dispatch(select(flowId))
store.dispatch(selectTab(tab))
}
}
if (query) {
query
.split("&")
.forEach((x) => {
const [key, value] = x.split("=", 2)
switch (key) {
case Query.SEARCH:
store.dispatch(updateFilter(value))
break
case Query.HIGHLIGHT:
store.dispatch(updateHighlight(value))
break
case Query.SHOW_EVENTLOG:
if(!store.getState().eventLog.visible)
store.dispatch(toggleVisibility(value))
break
default:
console.error(`unimplemented query arg: ${x}`)
}
})
}
}
function updateUrlFromStore(store) {
const state = store.getState()
let query = {
[Query.SEARCH]: state.flowView.filter,
[Query.HIGHLIGHT]: state.flowView.highlight,
[Query.SHOW_EVENTLOG]: state.eventLog.visible,
}
const queryStr = Object.keys(query)
.filter(k => query[k])
.map(k => `${k}=${query[k]}`)
.join("&")
let url
if (state.flows.selected.length > 0) {
url = `/flows/${state.flows.selected[0]}/${state.ui.flow.tab}`
} else {
url = "/flows"
}
if (queryStr) {
url += "?" + queryStr
}
if (window.location.hash !== url) {
// FIXME: replace state
window.location.hash = url
}
}
export default function initialize(store) {
updateStoreFromUrl(store)
store.subscribe(() => updateUrlFromStore(store))
}