mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-26 18:18:25 +00:00
commit
f0783a0874
@ -1,19 +1,18 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os.path
|
||||
import re
|
||||
import hashlib
|
||||
|
||||
|
||||
import tornado.websocket
|
||||
import tornado.web
|
||||
from io import BytesIO
|
||||
|
||||
from mitmproxy import flowfilter
|
||||
from mitmproxy import flow
|
||||
from mitmproxy import http
|
||||
import tornado.web
|
||||
import tornado.websocket
|
||||
import tornado.escape
|
||||
from mitmproxy import contentviews
|
||||
from mitmproxy import flow
|
||||
from mitmproxy import flowfilter
|
||||
from mitmproxy import http
|
||||
from mitmproxy import io
|
||||
from mitmproxy import version
|
||||
|
||||
@ -96,6 +95,14 @@ class BasicAuth:
|
||||
|
||||
class RequestHandler(BasicAuth, tornado.web.RequestHandler):
|
||||
|
||||
def write(self, chunk):
|
||||
# Writing arrays on the top level is ok nowadays.
|
||||
# http://flask.pocoo.org/docs/0.11/security/#json-security
|
||||
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,9 +191,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):
|
||||
@ -248,7 +253,9 @@ class FlowHandler(RequestHandler):
|
||||
elif k == "port":
|
||||
request.port = int(v)
|
||||
elif k == "headers":
|
||||
request.headers.set_state(v)
|
||||
request.headers.clear()
|
||||
for header in v:
|
||||
request.headers.add(*header)
|
||||
elif k == "content":
|
||||
request.text = v
|
||||
else:
|
||||
@ -264,7 +271,9 @@ class FlowHandler(RequestHandler):
|
||||
elif k == "http_version":
|
||||
response.http_version = str(v)
|
||||
elif k == "headers":
|
||||
response.headers.set_state(v)
|
||||
response.headers.clear()
|
||||
for header in v:
|
||||
response.headers.add(*header)
|
||||
elif k == "content":
|
||||
response.text = v
|
||||
else:
|
||||
@ -341,7 +350,7 @@ class FlowContentView(RequestHandler):
|
||||
message = getattr(self.flow, message)
|
||||
|
||||
description, lines, error = contentviews.get_message_content_view(
|
||||
contentviews.get(content_view.replace('_', ' ')).name, message
|
||||
content_view.replace('_', ' '), message
|
||||
)
|
||||
# if error:
|
||||
# add event log
|
||||
@ -355,31 +364,26 @@ class FlowContentView(RequestHandler):
|
||||
class Events(RequestHandler):
|
||||
|
||||
def get(self):
|
||||
self.write(dict(
|
||||
data=list([])
|
||||
))
|
||||
self.write([]) # FIXME
|
||||
|
||||
|
||||
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,
|
||||
showhost=self.master.options.showhost,
|
||||
no_upstream_cert=self.master.options.no_upstream_cert,
|
||||
rawtcp=self.master.options.rawtcp,
|
||||
http2=self.master.options.http2,
|
||||
anticache=self.master.options.anticache,
|
||||
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]
|
||||
)
|
||||
version=version.VERSION,
|
||||
mode=str(self.master.options.mode),
|
||||
intercept=self.master.options.intercept,
|
||||
showhost=self.master.options.showhost,
|
||||
no_upstream_cert=self.master.options.no_upstream_cert,
|
||||
rawtcp=self.master.options.rawtcp,
|
||||
http2=self.master.options.http2,
|
||||
anticache=self.master.options.anticache,
|
||||
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]
|
||||
))
|
||||
|
||||
def put(self):
|
||||
@ -419,7 +423,7 @@ class Settings(RequestHandler):
|
||||
print("Warning: Unknown setting {}: {}".format(k, v))
|
||||
|
||||
ClientConnection.broadcast(
|
||||
type="UPDATE_SETTINGS",
|
||||
resource="settings",
|
||||
cmd="update",
|
||||
data=update
|
||||
)
|
||||
|
@ -21,7 +21,8 @@ class Stop(Exception):
|
||||
|
||||
class _WebState():
|
||||
def add_log(self, e, level):
|
||||
self._last_event_id += 1
|
||||
# server-side log ids are odd
|
||||
self._last_event_id += 2
|
||||
entry = {
|
||||
"id": self._last_event_id,
|
||||
"message": e,
|
||||
@ -29,7 +30,7 @@ class _WebState():
|
||||
}
|
||||
self.events.append(entry)
|
||||
app.ClientConnection.broadcast(
|
||||
type="UPDATE_EVENTLOG",
|
||||
resource="events",
|
||||
cmd="add",
|
||||
data=entry
|
||||
)
|
||||
@ -38,9 +39,8 @@ class _WebState():
|
||||
super().clear()
|
||||
self.events.clear()
|
||||
app.ClientConnection.broadcast(
|
||||
type="UPDATE_EVENTLOG",
|
||||
cmd="reset",
|
||||
data=[]
|
||||
resource="events",
|
||||
cmd="reset"
|
||||
)
|
||||
|
||||
|
||||
@ -113,28 +113,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 one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,8 +1,8 @@
|
||||
|
||||
var conf = {
|
||||
src: "src/",
|
||||
dist: "../mitmproxy/web",
|
||||
static: "../mitmproxy/web/static",
|
||||
dist: "../mitmproxy/tools/web",
|
||||
static: "../mitmproxy/tools/web/static",
|
||||
js: {
|
||||
// Don't package these in the vendor distribution
|
||||
vendor_excludes: [
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
};
|
@ -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}>
|
||||
|
73
web/src/js/backends/websocket.js
Normal file
73
web/src/js/backends/websocket.js
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* The WebSocket backend is responsible for updating our knowledge of flows and events
|
||||
* from the REST API and live updates delivered via a WebSocket connection.
|
||||
* An alternative backend may use the REST API only to host static instances.
|
||||
*/
|
||||
import { fetchApi } from "../utils"
|
||||
|
||||
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, data) {
|
||||
let type = `${resource}_RECEIVE`.toUpperCase()
|
||||
this.store.dispatch({ type, cmd: "receive", resource, data })
|
||||
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)
|
||||
}
|
||||
}
|
@ -3,15 +3,15 @@ import { connect } from 'react-redux'
|
||||
import classnames from 'classnames'
|
||||
import columns from './FlowColumns'
|
||||
|
||||
import { updateSort } from '../../ducks/flowView'
|
||||
import { setSort } from '../../ducks/flows'
|
||||
|
||||
FlowTableHead.propTypes = {
|
||||
updateSort: PropTypes.func.isRequired,
|
||||
setSort: PropTypes.func.isRequired,
|
||||
sortDesc: React.PropTypes.bool.isRequired,
|
||||
sortColumn: React.PropTypes.string,
|
||||
}
|
||||
|
||||
function FlowTableHead({ sortColumn, sortDesc, updateSort }) {
|
||||
function FlowTableHead({ sortColumn, sortDesc, setSort }) {
|
||||
const sortType = sortDesc ? 'sort-desc' : 'sort-asc'
|
||||
|
||||
return (
|
||||
@ -19,7 +19,7 @@ function FlowTableHead({ sortColumn, sortDesc, updateSort }) {
|
||||
{columns.map(Column => (
|
||||
<th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)}
|
||||
key={Column.name}
|
||||
onClick={() => updateSort(Column.name, Column.name !== sortColumn ? false : !sortDesc)}>
|
||||
onClick={() => setSort(Column.name, Column.name !== sortColumn ? false : !sortDesc)}>
|
||||
{Column.headerName}
|
||||
</th>
|
||||
))}
|
||||
@ -29,10 +29,10 @@ function FlowTableHead({ sortColumn, sortDesc, updateSort }) {
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
sortDesc: state.flowView.sort.desc,
|
||||
sortColumn: state.flowView.sort.column,
|
||||
sortDesc: state.flows.sort.desc,
|
||||
sortColumn: state.flows.sort.column,
|
||||
}),
|
||||
{
|
||||
updateSort
|
||||
setSort
|
||||
}
|
||||
)(FlowTableHead)
|
||||
|
@ -2,7 +2,7 @@ import React, { Component, PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import FilterInput from './FilterInput'
|
||||
import { update as updateSettings } from '../../ducks/settings'
|
||||
import { updateFilter, updateHighlight } from '../../ducks/flowView'
|
||||
import { setFilter, setHighlight } from '../../ducks/flows'
|
||||
|
||||
MainMenu.title = "Start"
|
||||
|
||||
@ -31,20 +31,20 @@ const InterceptInput = connect(
|
||||
|
||||
const FlowFilterInput = connect(
|
||||
state => ({
|
||||
value: state.flowView.filter || '',
|
||||
value: state.flows.filter || '',
|
||||
placeholder: 'Search',
|
||||
type: 'search',
|
||||
color: 'black'
|
||||
}),
|
||||
{ onChange: updateFilter }
|
||||
{ onChange: setFilter }
|
||||
)(FilterInput);
|
||||
|
||||
const HighlightInput = connect(
|
||||
state => ({
|
||||
value: state.flowView.highlight || '',
|
||||
value: state.flows.highlight || '',
|
||||
placeholder: 'Highlight',
|
||||
type: 'tag',
|
||||
color: 'hsl(48, 100%, 50%)'
|
||||
}),
|
||||
{ onChange: updateHighlight }
|
||||
{ onChange: setHighlight }
|
||||
)(FilterInput);
|
||||
|
@ -4,7 +4,6 @@ import Splitter from './common/Splitter'
|
||||
import FlowTable from './FlowTable'
|
||||
import FlowView from './FlowView'
|
||||
import * as flowsActions from '../ducks/flows'
|
||||
import { updateFilter, updateHighlight } from '../ducks/flowView'
|
||||
|
||||
class MainView extends Component {
|
||||
|
||||
@ -41,16 +40,14 @@ class MainView extends Component {
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
flows: state.flowView.data,
|
||||
filter: state.flowView.filter,
|
||||
highlight: state.flowView.highlight,
|
||||
flows: state.flows.view,
|
||||
filter: state.flows.filter,
|
||||
highlight: state.flows.highlight,
|
||||
selectedFlow: state.flows.byId[state.flows.selected[0]],
|
||||
tab: state.ui.flow.tab,
|
||||
}),
|
||||
{
|
||||
selectFlow: flowsActions.select,
|
||||
updateFilter,
|
||||
updateHighlight,
|
||||
updateFlow: flowsActions.update,
|
||||
}
|
||||
)(MainView)
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
};
|
@ -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 })
|
||||
}
|
||||
}
|
@ -1,24 +1,15 @@
|
||||
import reduceList, * as listActions from './utils/list'
|
||||
import reduceView, * as viewActions from './utils/view'
|
||||
import * as websocketActions from './websocket'
|
||||
import * as msgQueueActions from './msgQueue'
|
||||
import reduceStore from "./utils/store"
|
||||
import * as storeActions from "./utils/store"
|
||||
|
||||
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'
|
||||
|
||||
const defaultState = {
|
||||
logId: 0,
|
||||
visible: false,
|
||||
filters: { debug: false, info: true, web: true },
|
||||
list: reduceList(undefined, {}),
|
||||
view: reduceView(undefined, {}),
|
||||
...reduceStore(undefined, {}),
|
||||
}
|
||||
|
||||
export default function reduce(state = defaultState, action) {
|
||||
@ -35,27 +26,14 @@ export default function reduce(state = defaultState, action) {
|
||||
return {
|
||||
...state,
|
||||
filters,
|
||||
view: reduceView(state.view, viewActions.updateFilter(state.list.data, log => filters[log.level])),
|
||||
...reduceStore(state, storeActions.setFilter(log => filters[log.level]))
|
||||
}
|
||||
|
||||
case ADD:
|
||||
const item = {
|
||||
id: state.logId,
|
||||
message: action.message,
|
||||
level: action.level,
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
logId: state.logId + 1,
|
||||
list: reduceList(state.list, listActions.add(item)),
|
||||
view: reduceView(state.view, viewActions.add(item, log => state.filters[log.level])),
|
||||
}
|
||||
|
||||
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])),
|
||||
...reduceStore(state, storeActions[action.cmd](action.data, log => state.filters[log.level]))
|
||||
}
|
||||
|
||||
default:
|
||||
@ -63,58 +41,25 @@ 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
|
||||
*/
|
||||
let logId = 1 // client-side log ids are odd
|
||||
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 }
|
||||
let data = {
|
||||
id: logId,
|
||||
message,
|
||||
level,
|
||||
}
|
||||
logId += 2
|
||||
return {
|
||||
type: ADD,
|
||||
cmd: "add",
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public websocket
|
||||
*/
|
||||
export function fetchData() {
|
||||
return msgQueueActions.fetchData(MSG_TYPE)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public msgQueue
|
||||
*/
|
||||
export function receiveData(list) {
|
||||
return { type: RECEIVE, list }
|
||||
}
|
||||
|
@ -1,195 +0,0 @@
|
||||
import reduceView, * as viewActions from './utils/view'
|
||||
import * as flowActions from './flows'
|
||||
import Filt from '../filt/filt'
|
||||
import { RequestUtils } from '../flow/utils'
|
||||
|
||||
export const UPDATE_FILTER = 'FLOWVIEW_UPDATE_FILTER'
|
||||
export const UPDATE_SORT = 'FLOWVIEW_UPDATE_SORT'
|
||||
export const UPDATE_HIGHLIGHT = 'FLOWVIEW_UPDATE_HIGHLIGHT'
|
||||
|
||||
|
||||
const sortKeyFuns = {
|
||||
|
||||
TLSColumn: flow => flow.request.scheme,
|
||||
|
||||
PathColumn: flow => RequestUtils.pretty_url(flow.request),
|
||||
|
||||
MethodColumn: flow => flow.request.method,
|
||||
|
||||
StatusColumn: flow => flow.response && flow.response.status_code,
|
||||
|
||||
TimeColumn: flow => flow.response && flow.response.timestamp_end - flow.request.timestamp_start,
|
||||
|
||||
SizeColumn: flow => {
|
||||
let total = flow.request.contentLength
|
||||
if (flow.response) {
|
||||
total += flow.response.contentLength || 0
|
||||
}
|
||||
return total
|
||||
},
|
||||
}
|
||||
|
||||
export function makeFilter(filter) {
|
||||
if (!filter) {
|
||||
return
|
||||
}
|
||||
return Filt.parse(filter)
|
||||
}
|
||||
|
||||
export function makeSort({ column, desc }) {
|
||||
const sortKeyFun = sortKeyFuns[column]
|
||||
if (!sortKeyFun) {
|
||||
return
|
||||
}
|
||||
return (a, b) => {
|
||||
const ka = sortKeyFun(a)
|
||||
const kb = sortKeyFun(b)
|
||||
if (ka > kb) {
|
||||
return desc ? -1 : 1
|
||||
}
|
||||
if (ka < kb) {
|
||||
return desc ? 1 : -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const defaultState = {
|
||||
highlight: null,
|
||||
filter: null,
|
||||
sort: { column: null, desc: false },
|
||||
...reduceView(undefined, {})
|
||||
}
|
||||
|
||||
export default function reduce(state = defaultState, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case UPDATE_HIGHLIGHT:
|
||||
return {
|
||||
...state,
|
||||
highlight: action.highlight,
|
||||
}
|
||||
|
||||
case UPDATE_FILTER:
|
||||
return {
|
||||
...reduceView(
|
||||
state,
|
||||
viewActions.updateFilter(
|
||||
action.flows,
|
||||
makeFilter(action.filter),
|
||||
makeSort(state.sort)
|
||||
)
|
||||
),
|
||||
filter: action.filter,
|
||||
}
|
||||
|
||||
case UPDATE_SORT:
|
||||
const sort = { column: action.column, desc: action.desc }
|
||||
return {
|
||||
...reduceView(
|
||||
state,
|
||||
viewActions.updateSort(
|
||||
makeSort(sort)
|
||||
)
|
||||
),
|
||||
sort,
|
||||
}
|
||||
|
||||
case flowActions.ADD:
|
||||
return {
|
||||
...reduceView(
|
||||
state,
|
||||
viewActions.add(
|
||||
action.item,
|
||||
makeFilter(state.filter),
|
||||
makeSort(state.sort)
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
case flowActions.UPDATE:
|
||||
return {
|
||||
...reduceView(
|
||||
state,
|
||||
viewActions.update(
|
||||
action.item,
|
||||
makeFilter(state.filter),
|
||||
makeSort(state.sort)
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
case flowActions.REMOVE:
|
||||
return {
|
||||
...reduceView(
|
||||
state,
|
||||
viewActions.remove(
|
||||
action.id
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
case flowActions.RECEIVE:
|
||||
return {
|
||||
...reduceView(
|
||||
state,
|
||||
viewActions.receive(
|
||||
action.list,
|
||||
makeFilter(state.filter),
|
||||
makeSort(state.sort)
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
...reduceView(state, 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]]
|
||||
let minIndex = 0
|
||||
let maxIndex = getState().flowView.data.length - 1
|
||||
let newIndex
|
||||
if (currentSelectionIndex === undefined) {
|
||||
newIndex = (shift < 0) ? minIndex : maxIndex
|
||||
} else {
|
||||
newIndex = currentSelectionIndex + shift
|
||||
newIndex = Math.max(newIndex, minIndex)
|
||||
newIndex = Math.min(newIndex, maxIndex)
|
||||
}
|
||||
let flow = getState().flowView.data[newIndex]
|
||||
dispatch(flowActions.select(flow ? flow.id : undefined))
|
||||
}
|
||||
}
|
@ -1,53 +1,58 @@
|
||||
import { fetchApi } from '../utils'
|
||||
import reduceList, * as listActions from './utils/list'
|
||||
import { selectRelative } from './flowView'
|
||||
import { fetchApi } from "../utils"
|
||||
import reduceStore, * as storeActions from "./utils/store"
|
||||
import Filt from "../filt/filt"
|
||||
import { RequestUtils } from "../flow/utils"
|
||||
|
||||
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'
|
||||
export const RECEIVE = 'FLOWS_RECEIVE'
|
||||
export const ADD = 'FLOWS_ADD'
|
||||
export const UPDATE = 'FLOWS_UPDATE'
|
||||
export const REMOVE = 'FLOWS_REMOVE'
|
||||
export const RECEIVE = 'FLOWS_RECEIVE'
|
||||
export const SELECT = 'FLOWS_SELECT'
|
||||
export const SET_FILTER = 'FLOWS_SET_FILTER'
|
||||
export const SET_SORT = 'FLOWS_SET_SORT'
|
||||
export const SET_HIGHLIGHT = 'FLOWS_SET_HIGHLIGHT'
|
||||
export const REQUEST_ACTION = 'FLOWS_REQUEST_ACTION'
|
||||
export const UNKNOWN_CMD = 'FLOWS_UNKNOWN_CMD'
|
||||
export const FETCH_ERROR = 'FLOWS_FETCH_ERROR'
|
||||
export const SELECT = 'FLOWS_SELECT'
|
||||
|
||||
|
||||
const defaultState = {
|
||||
highlight: null,
|
||||
filter: null,
|
||||
sort: { column: null, desc: false },
|
||||
selected: [],
|
||||
...reduceList(undefined, {}),
|
||||
...reduceStore(undefined, {})
|
||||
}
|
||||
|
||||
export default function reduce(state = defaultState, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case ADD:
|
||||
return {
|
||||
...state,
|
||||
...reduceList(state, listActions.add(action.item)),
|
||||
}
|
||||
|
||||
case UPDATE:
|
||||
return {
|
||||
...state,
|
||||
...reduceList(state, listActions.update(action.item)),
|
||||
}
|
||||
|
||||
case REMOVE:
|
||||
case RECEIVE:
|
||||
// FIXME: Update state.selected on REMOVE:
|
||||
// The selected flow may have been removed, we need to select the next one in the view.
|
||||
let storeAction = storeActions[action.cmd](
|
||||
action.data,
|
||||
makeFilter(state.filter),
|
||||
makeSort(state.sort)
|
||||
)
|
||||
return {
|
||||
...state,
|
||||
...reduceList(state, listActions.remove(action.id)),
|
||||
...reduceStore(state, storeAction)
|
||||
}
|
||||
|
||||
case RECEIVE:
|
||||
case SET_FILTER:
|
||||
return {
|
||||
...state,
|
||||
...reduceList(state, listActions.receive(action.list)),
|
||||
filter: action.filter,
|
||||
...reduceStore(state, storeActions.setFilter(makeFilter(action.filter), makeSort(state.sort)))
|
||||
}
|
||||
|
||||
case SET_SORT:
|
||||
return {
|
||||
...state,
|
||||
sort: action.sort,
|
||||
...reduceStore(state, storeActions.setSort(makeSort(action.sort)))
|
||||
}
|
||||
|
||||
case SELECT:
|
||||
@ -57,88 +62,133 @@ export default function reduce(state = defaultState, action) {
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
...state,
|
||||
...reduceList(state, action),
|
||||
}
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
||||
const sortKeyFuns = {
|
||||
|
||||
TLSColumn: flow => flow.request.scheme,
|
||||
|
||||
PathColumn: flow => RequestUtils.pretty_url(flow.request),
|
||||
|
||||
MethodColumn: flow => flow.request.method,
|
||||
|
||||
StatusColumn: flow => flow.response && flow.response.status_code,
|
||||
|
||||
TimeColumn: flow => flow.response && flow.response.timestamp_end - flow.request.timestamp_start,
|
||||
|
||||
SizeColumn: flow => {
|
||||
let total = flow.request.contentLength
|
||||
if (flow.response) {
|
||||
total += flow.response.contentLength || 0
|
||||
}
|
||||
return total
|
||||
},
|
||||
}
|
||||
|
||||
export function makeFilter(filter) {
|
||||
if (!filter) {
|
||||
return
|
||||
}
|
||||
return Filt.parse(filter)
|
||||
}
|
||||
|
||||
export function makeSort({ column, desc }) {
|
||||
const sortKeyFun = sortKeyFuns[column]
|
||||
if (!sortKeyFun) {
|
||||
return
|
||||
}
|
||||
return (a, b) => {
|
||||
const ka = sortKeyFun(a)
|
||||
const kb = sortKeyFun(b)
|
||||
if (ka > kb) {
|
||||
return desc ? -1 : 1
|
||||
}
|
||||
if (ka < kb) {
|
||||
return desc ? 1 : -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
export function setFilter(filter) {
|
||||
return { type: SET_FILTER, filter }
|
||||
}
|
||||
|
||||
export function setHighlight(highlight) {
|
||||
return { type: SET_HIGHLIGHT, highlight }
|
||||
}
|
||||
|
||||
export function setSort(column, desc) {
|
||||
return { type: SET_SORT, sort: { column, desc } }
|
||||
}
|
||||
|
||||
export function selectRelative(shift) {
|
||||
return (dispatch, getState) => {
|
||||
let currentSelectionIndex = getState().flows.viewIndex[getState().flows.selected[0]]
|
||||
let minIndex = 0
|
||||
let maxIndex = getState().flows.view.length - 1
|
||||
let newIndex
|
||||
if (currentSelectionIndex === undefined) {
|
||||
newIndex = (shift < 0) ? minIndex : maxIndex
|
||||
} else {
|
||||
newIndex = currentSelectionIndex + shift
|
||||
newIndex = window.Math.max(newIndex, minIndex)
|
||||
newIndex = window.Math.min(newIndex, maxIndex)
|
||||
}
|
||||
let flow = getState().flows.view[newIndex]
|
||||
dispatch(select(flow ? flow.id : undefined))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
export function uploadContent(flow, file, type) {
|
||||
const body = new FormData()
|
||||
file = new Blob([file], {type: 'plain/text'})
|
||||
file = new window.Blob([file], { type: 'plain/text' })
|
||||
body.append('file', file)
|
||||
return dispatch => fetchApi(`/flows/${flow.id}/${type}/content`, {method: 'post', body} )
|
||||
return dispatch => fetchApi(`/flows/${flow.id}/${type}/content`, { method: 'post', body })
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @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 +202,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 })
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,12 @@
|
||||
import { combineReducers } from 'redux'
|
||||
import eventLog from './eventLog'
|
||||
import websocket from './websocket'
|
||||
import flows from './flows'
|
||||
import flowView from './flowView'
|
||||
import settings from './settings'
|
||||
import ui from './ui/index'
|
||||
import msgQueue from './msgQueue'
|
||||
|
||||
export default combineReducers({
|
||||
eventLog,
|
||||
websocket,
|
||||
flows,
|
||||
flowView,
|
||||
settings,
|
||||
ui,
|
||||
msgQueue,
|
||||
})
|
||||
|
@ -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 }
|
||||
}
|
@ -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'
|
||||
|
||||
@ -18,12 +13,12 @@ export default function reducer(state = defaultState, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case RECEIVE:
|
||||
return action.settings
|
||||
return action.data
|
||||
|
||||
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 }
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ export default function reducer(state = defaultState, action) {
|
||||
// There is no explicit "stop edit" event.
|
||||
// We stop editing when we receive an update for
|
||||
// the currently edited flow from the server
|
||||
if (action.item.id === state.modifiedFlow.id) {
|
||||
if (action.data.id === state.modifiedFlow.id) {
|
||||
return {
|
||||
...state,
|
||||
modifiedFlow: false,
|
||||
|
@ -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,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Key } from '../../utils'
|
||||
import { selectRelative as selectFlowRelative } from '../flowView'
|
||||
import { selectTab } from './flow'
|
||||
import * as flowsActions from '../flows'
|
||||
|
||||
@ -20,29 +19,29 @@ export function onKeyDown(e) {
|
||||
switch (key) {
|
||||
case Key.K:
|
||||
case Key.UP:
|
||||
dispatch(selectFlowRelative(-1))
|
||||
dispatch(flowsActions.selectRelative(-1))
|
||||
break
|
||||
|
||||
case Key.J:
|
||||
case Key.DOWN:
|
||||
dispatch(selectFlowRelative(+1))
|
||||
dispatch(flowsActions.selectRelative(+1))
|
||||
break
|
||||
|
||||
case Key.SPACE:
|
||||
case Key.PAGE_DOWN:
|
||||
dispatch(selectFlowRelative(+10))
|
||||
dispatch(flowsActions.selectRelative(+10))
|
||||
break
|
||||
|
||||
case Key.PAGE_UP:
|
||||
dispatch(selectFlowRelative(-10))
|
||||
dispatch(flowsActions.selectRelative(-10))
|
||||
break
|
||||
|
||||
case Key.END:
|
||||
dispatch(selectFlowRelative(+1e10))
|
||||
dispatch(flowsActions.selectRelative(+1e10))
|
||||
break
|
||||
|
||||
case Key.HOME:
|
||||
dispatch(selectFlowRelative(-1e10))
|
||||
dispatch(flowsActions.selectRelative(-1e10))
|
||||
break
|
||||
|
||||
case Key.ESC:
|
||||
|
@ -1,105 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
export const ADD = 'LIST_ADD'
|
||||
export const UPDATE = 'LIST_UPDATE'
|
||||
export const REMOVE = 'LIST_REMOVE'
|
||||
export const RECEIVE = 'LIST_RECEIVE'
|
||||
|
||||
const defaultState = {
|
||||
data: [],
|
||||
byId: {},
|
||||
indexOf: {},
|
||||
}
|
||||
|
||||
export default function reduce(state = defaultState, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case ADD:
|
||||
return {
|
||||
...state,
|
||||
data: [...state.data, action.item],
|
||||
byId: { ...state.byId, [action.item.id]: action.item },
|
||||
indexOf: { ...state.indexOf, [action.item.id]: state.data.length },
|
||||
}
|
||||
|
||||
case UPDATE: {
|
||||
const index = state.indexOf[action.item.id]
|
||||
|
||||
if (index == null) {
|
||||
return state
|
||||
}
|
||||
|
||||
const data = [...state.data]
|
||||
|
||||
data[index] = action.item
|
||||
|
||||
return {
|
||||
...state,
|
||||
data,
|
||||
byId: { ...state.byId, [action.item.id]: action.item }
|
||||
}
|
||||
}
|
||||
|
||||
case REMOVE: {
|
||||
const index = state.indexOf[action.id]
|
||||
|
||||
if (index == null) {
|
||||
return state
|
||||
}
|
||||
|
||||
const data = [...state.data]
|
||||
const indexOf = { ...state.indexOf, [action.id]: null }
|
||||
|
||||
data.splice(index, 1)
|
||||
for (let i = data.length - 1; i >= index; i--) {
|
||||
indexOf[data[i].id] = i
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
data,
|
||||
indexOf,
|
||||
byId: { ...state.byId, [action.id]: null },
|
||||
}
|
||||
}
|
||||
|
||||
case RECEIVE:
|
||||
return {
|
||||
...state,
|
||||
data: action.list,
|
||||
byId: _.fromPairs(action.list.map(item => [item.id, item])),
|
||||
indexOf: _.fromPairs(action.list.map((item, index) => [item.id, index])),
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 }
|
||||
}
|
210
web/src/js/ducks/utils/store.js
Normal file
210
web/src/js/ducks/utils/store.js
Normal file
@ -0,0 +1,210 @@
|
||||
export const SET_FILTER = 'LIST_SET_FILTER'
|
||||
export const SET_SORT = 'LIST_SET_SORT'
|
||||
export const ADD = 'LIST_ADD'
|
||||
export const UPDATE = 'LIST_UPDATE'
|
||||
export const REMOVE = 'LIST_REMOVE'
|
||||
export const RECEIVE = 'LIST_RECEIVE'
|
||||
|
||||
const defaultState = {
|
||||
byId: {},
|
||||
list: [],
|
||||
listIndex: {},
|
||||
view: [],
|
||||
viewIndex: {},
|
||||
}
|
||||
|
||||
/**
|
||||
* The store reducer can be used as a mixin to another reducer that always returns a
|
||||
* new { byId, list, listIndex, view, viewIndex } object. The reducer using the store
|
||||
* usually has to map its action to the matching store action and then call the mixin with that.
|
||||
*
|
||||
* Example Usage:
|
||||
*
|
||||
* import reduceStore, * as storeActions from "./utils/store"
|
||||
*
|
||||
* case EVENTLOG_ADD:
|
||||
* return {
|
||||
* ...state,
|
||||
* ...reduceStore(state, storeActions.add(action.data))
|
||||
* }
|
||||
*
|
||||
*/
|
||||
export default function reduce(state = defaultState, action) {
|
||||
|
||||
let { byId, list, listIndex, view, viewIndex } = state
|
||||
|
||||
switch (action.type) {
|
||||
case SET_FILTER:
|
||||
view = list.filter(action.filter).sort(action.sort)
|
||||
viewIndex = {}
|
||||
view.forEach((item, index) => {
|
||||
viewIndex[item.id] = index
|
||||
})
|
||||
break
|
||||
|
||||
case SET_SORT:
|
||||
view = [...view].sort(action.sort)
|
||||
viewIndex = {}
|
||||
view.forEach((item, index) => {
|
||||
viewIndex[item.id] = index
|
||||
})
|
||||
break
|
||||
|
||||
case ADD:
|
||||
if (action.item.id in byId) {
|
||||
// we already had that.
|
||||
break
|
||||
}
|
||||
byId = { ...byId, [action.item.id]: action.item }
|
||||
listIndex = { ...listIndex, [action.item.id]: list.length }
|
||||
list = [...list, action.item]
|
||||
if (action.filter(action.item)) {
|
||||
({ view, viewIndex } = sortedInsert(state, action.item, action.sort))
|
||||
}
|
||||
break
|
||||
|
||||
case UPDATE:
|
||||
byId = { ...byId, [action.item.id]: action.item }
|
||||
list = [...list]
|
||||
list[listIndex[action.item.id]] = action.item
|
||||
|
||||
let hasOldItem = action.item.id in viewIndex
|
||||
let hasNewItem = action.filter(action.item)
|
||||
if (hasNewItem && !hasOldItem) {
|
||||
({view, viewIndex} = sortedInsert(state, action.item, action.sort))
|
||||
}
|
||||
else if (!hasNewItem && hasOldItem) {
|
||||
({data: view, dataIndex: viewIndex} = removeData(view, viewIndex, action.item.id))
|
||||
}
|
||||
else if (hasNewItem && hasOldItem) {
|
||||
({view, viewIndex} = sortedUpdate(state, action.item, action.sort))
|
||||
}
|
||||
break
|
||||
|
||||
case REMOVE:
|
||||
if (!(action.id in byId)) {
|
||||
break
|
||||
}
|
||||
delete byId[action.id];
|
||||
({data: list, dataIndex: listIndex} = removeData(list, listIndex, action.id))
|
||||
|
||||
if (action.id in viewIndex) {
|
||||
({data: view, dataIndex: viewIndex} = removeData(view, viewIndex, action.id))
|
||||
}
|
||||
break
|
||||
|
||||
case RECEIVE:
|
||||
list = action.list
|
||||
listIndex = {}
|
||||
byId = {}
|
||||
list.forEach((item, i) => {
|
||||
byId[item.id] = item
|
||||
listIndex[item.id] = i
|
||||
})
|
||||
view = list.filter(action.filter).sort(action.sort)
|
||||
viewIndex = {}
|
||||
view.forEach((item, index) => {
|
||||
viewIndex[item.id] = index
|
||||
})
|
||||
break
|
||||
}
|
||||
return { byId, list, listIndex, view, viewIndex }
|
||||
}
|
||||
|
||||
|
||||
export function setFilter(filter = defaultFilter, sort = defaultSort) {
|
||||
return { type: SET_FILTER, filter, sort }
|
||||
}
|
||||
|
||||
export function setSort(sort = defaultSort) {
|
||||
return { type: SET_SORT, sort }
|
||||
}
|
||||
|
||||
export function add(item, filter = defaultFilter, sort = defaultSort) {
|
||||
return { type: ADD, item, filter, sort }
|
||||
}
|
||||
|
||||
export function update(item, filter = defaultFilter, sort = defaultSort) {
|
||||
return { type: UPDATE, item, filter, sort }
|
||||
}
|
||||
|
||||
export function remove(id) {
|
||||
return { type: REMOVE, id }
|
||||
}
|
||||
|
||||
export function receive(list, filter = defaultFilter, sort = defaultSort) {
|
||||
return { type: RECEIVE, list, filter, sort }
|
||||
}
|
||||
|
||||
function sortedInsert(state, item, sort) {
|
||||
const index = sortedIndex(state.view, item, sort)
|
||||
const view = [...state.view]
|
||||
const viewIndex = { ...state.viewIndex }
|
||||
|
||||
view.splice(index, 0, item)
|
||||
for (let i = view.length - 1; i >= index; i--) {
|
||||
viewIndex[view[i].id] = i
|
||||
}
|
||||
|
||||
return { view, viewIndex }
|
||||
}
|
||||
|
||||
function removeData(currentData, currentDataIndex, id) {
|
||||
const index = currentDataIndex[id]
|
||||
const data = [...currentData]
|
||||
const dataIndex = { ...currentDataIndex }
|
||||
delete dataIndex[id];
|
||||
|
||||
data.splice(index, 1)
|
||||
for (let i = data.length - 1; i >= index; i--) {
|
||||
dataIndex[data[i].id] = i
|
||||
}
|
||||
|
||||
return { data, dataIndex }
|
||||
}
|
||||
|
||||
function sortedUpdate(state, item, sort) {
|
||||
let view = [...state.view]
|
||||
let viewIndex = { ...state.viewIndex }
|
||||
let index = viewIndex[item.id]
|
||||
view[index] = item
|
||||
while (index + 1 < view.length && sort(view[index], view[index + 1]) > 0) {
|
||||
view[index] = view[index + 1]
|
||||
view[index + 1] = item
|
||||
viewIndex[item.id] = index + 1
|
||||
viewIndex[view[index].id] = index
|
||||
++index
|
||||
}
|
||||
while (index > 0 && sort(view[index], view[index - 1]) < 0) {
|
||||
view[index] = view[index - 1]
|
||||
view[index - 1] = item
|
||||
viewIndex[item.id] = index - 1
|
||||
viewIndex[view[index].id] = index
|
||||
--index
|
||||
}
|
||||
return { view, viewIndex }
|
||||
}
|
||||
|
||||
function sortedIndex(list, item, sort) {
|
||||
let low = 0
|
||||
let high = list.length
|
||||
|
||||
while (low < high) {
|
||||
const middle = (low + high) >>> 1
|
||||
if (sort(item, list[middle]) >= 0) {
|
||||
low = middle + 1
|
||||
} else {
|
||||
high = middle
|
||||
}
|
||||
}
|
||||
|
||||
return low
|
||||
}
|
||||
|
||||
function defaultFilter() {
|
||||
return true
|
||||
}
|
||||
|
||||
function defaultSort(a, b) {
|
||||
return 0
|
||||
}
|
@ -1,189 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
export const UPDATE_FILTER = 'VIEW_UPDATE_FILTER'
|
||||
export const UPDATE_SORT = 'VIEW_UPDATE_SORT'
|
||||
export const ADD = 'VIEW_ADD'
|
||||
export const UPDATE = 'VIEW_UPDATE'
|
||||
export const REMOVE = 'VIEW_REMOVE'
|
||||
export const RECEIVE = 'VIEW_RECEIVE'
|
||||
|
||||
const defaultState = {
|
||||
data: [],
|
||||
indexOf: {},
|
||||
}
|
||||
|
||||
export default function reduce(state = defaultState, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case UPDATE_FILTER:
|
||||
{
|
||||
const data = action.list.filter(action.filter).sort(action.sort)
|
||||
return {
|
||||
...state,
|
||||
data,
|
||||
indexOf: _.fromPairs(data.map((item, index) => [item.id, index])),
|
||||
}
|
||||
}
|
||||
|
||||
case UPDATE_SORT:
|
||||
{
|
||||
const data = [...state.data].sort(action.sort)
|
||||
return {
|
||||
...state,
|
||||
data,
|
||||
indexOf: _.fromPairs(data.map((item, index) => [item.id, index])),
|
||||
}
|
||||
}
|
||||
|
||||
case ADD:
|
||||
if (state.indexOf[action.item.id] != null || !action.filter(action.item)) {
|
||||
return state
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
...sortedInsert(state, action.item, action.sort),
|
||||
}
|
||||
|
||||
case REMOVE:
|
||||
if (state.indexOf[action.id] == null) {
|
||||
return state
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
...sortedRemove(state, action.id),
|
||||
}
|
||||
|
||||
case UPDATE:
|
||||
let hasOldItem = state.indexOf[action.item.id] !== null && state.indexOf[action.item.id] !== undefined
|
||||
let hasNewItem = action.filter(action.item)
|
||||
if (!hasNewItem && !hasOldItem) {
|
||||
return state
|
||||
}
|
||||
if (hasNewItem && !hasOldItem) {
|
||||
return {
|
||||
...state,
|
||||
...sortedInsert(state, action.item, action.sort)
|
||||
}
|
||||
}
|
||||
if (!hasNewItem && hasOldItem) {
|
||||
return {
|
||||
...state,
|
||||
...sortedRemove(state, action.item.id)
|
||||
}
|
||||
}
|
||||
if (hasNewItem && hasOldItem) {
|
||||
return {
|
||||
...state,
|
||||
...sortedUpdate(state, action.item, action.sort),
|
||||
}
|
||||
}
|
||||
case RECEIVE:
|
||||
{
|
||||
const data = action.list.filter(action.filter).sort(action.sort)
|
||||
return {
|
||||
...state,
|
||||
data,
|
||||
indexOf: _.fromPairs(data.map((item, index) => [item.id, index])),
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export function updateFilter(list, filter = defaultFilter, sort = defaultSort) {
|
||||
return { type: UPDATE_FILTER, list, filter, sort }
|
||||
}
|
||||
|
||||
export function updateSort(sort = defaultSort) {
|
||||
return { type: UPDATE_SORT, sort }
|
||||
}
|
||||
|
||||
export function add(item, filter = defaultFilter, sort = defaultSort) {
|
||||
return { type: ADD, item, filter, sort }
|
||||
}
|
||||
|
||||
export function update(item, filter = defaultFilter, sort = defaultSort) {
|
||||
return { type: UPDATE, item, filter, sort }
|
||||
}
|
||||
|
||||
export function remove(id) {
|
||||
return { type: REMOVE, id }
|
||||
}
|
||||
|
||||
export function receive(list, filter = defaultFilter, sort = defaultSort) {
|
||||
return { type: RECEIVE, list, filter, sort }
|
||||
}
|
||||
|
||||
function sortedInsert(state, item, sort) {
|
||||
const index = sortedIndex(state.data, item, sort)
|
||||
const data = [ ...state.data ]
|
||||
const indexOf = { ...state.indexOf }
|
||||
|
||||
data.splice(index, 0, item)
|
||||
for (let i = data.length - 1; i >= index; i--) {
|
||||
indexOf[data[i].id] = i
|
||||
}
|
||||
|
||||
return { data, indexOf }
|
||||
}
|
||||
|
||||
function sortedRemove(state, id) {
|
||||
const index = state.indexOf[id]
|
||||
const data = [...state.data]
|
||||
const indexOf = { ...state.indexOf, [id]: null }
|
||||
|
||||
data.splice(index, 1)
|
||||
for (let i = data.length - 1; i >= index; i--) {
|
||||
indexOf[data[i].id] = i
|
||||
}
|
||||
|
||||
return { data, indexOf }
|
||||
}
|
||||
|
||||
function sortedUpdate(state, item, sort) {
|
||||
let data = [ ...state.data ]
|
||||
let indexOf = { ...state.indexOf }
|
||||
let index = indexOf[item.id]
|
||||
data[index] = item
|
||||
while (index + 1 < data.length && sort(data[index], data[index + 1]) > 0) {
|
||||
data[index] = data[index + 1]
|
||||
data[index + 1] = item
|
||||
indexOf[item.id] = index + 1
|
||||
indexOf[data[index].id] = index
|
||||
++index
|
||||
}
|
||||
while (index > 0 && sort(data[index], data[index - 1]) < 0) {
|
||||
data[index] = data[index - 1]
|
||||
data[index - 1] = item
|
||||
indexOf[item.id] = index - 1
|
||||
indexOf[data[index].id] = index
|
||||
--index
|
||||
}
|
||||
return { data, indexOf }
|
||||
}
|
||||
|
||||
function sortedIndex(list, item, sort) {
|
||||
let low = 0
|
||||
let high = list.length
|
||||
|
||||
while (low < high) {
|
||||
const middle = (low + high) >>> 1
|
||||
if (sort(item, list[middle]) >= 0) {
|
||||
low = middle + 1
|
||||
} else {
|
||||
high = middle
|
||||
}
|
||||
}
|
||||
|
||||
return low
|
||||
}
|
||||
|
||||
function defaultFilter() {
|
||||
return true
|
||||
}
|
||||
|
||||
function defaultSort(a, b) {
|
||||
return 0
|
||||
}
|
@ -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 })
|
||||
}
|
||||
}
|
83
web/src/js/urlState.js
Normal file
83
web/src/js/urlState.js
Normal file
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Instead of dealing with react-router's ever-changing APIs,
|
||||
* we use a simple url state manager where we only
|
||||
*
|
||||
* - read the initial URL state on page load
|
||||
* - push updates to the URL later on.
|
||||
*/
|
||||
import { select, setFilter, setHighlight } from "./ducks/flows"
|
||||
import { selectTab } from "./ducks/ui/flow"
|
||||
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(setFilter(value))
|
||||
break
|
||||
case Query.HIGHLIGHT:
|
||||
store.dispatch(setHighlight(value))
|
||||
break
|
||||
case Query.SHOW_EVENTLOG:
|
||||
if (!store.getState().eventLog.visible)
|
||||
store.dispatch(toggleVisibility())
|
||||
break
|
||||
default:
|
||||
console.error(`unimplemented query arg: ${x}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function updateUrlFromStore(store) {
|
||||
const state = store.getState()
|
||||
let query = {
|
||||
[Query.SEARCH]: state.flows.filter,
|
||||
[Query.HIGHLIGHT]: state.flows.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.substr(1) !== url) {
|
||||
history.replaceState(undefined, "", `/#${url}`)
|
||||
}
|
||||
}
|
||||
|
||||
export default function initialize(store) {
|
||||
updateStoreFromUrl(store)
|
||||
store.subscribe(() => updateUrlFromStore(store))
|
||||
}
|
Loading…
Reference in New Issue
Block a user