Merge pull request #414 from mitmproxy/flowviews2

Flowviews2
This commit is contained in:
Aldo Cortesi 2014-12-12 22:08:15 +13:00
commit 01fa5d3f07
49 changed files with 16997 additions and 30742 deletions

View File

@ -277,16 +277,16 @@ class ConsoleState(flow.State):
d = self.flowsettings.get(flow, {})
return d.get(key, default)
def add_request(self, f):
flow.State.add_request(self, f)
def add_flow(self, f):
super(ConsoleState, self).add_flow(f)
if self.focus is None:
self.set_focus(0)
elif self.follow_focus:
self.set_focus(len(self.view) - 1)
return f
def add_response(self, resp):
f = flow.State.add_response(self, resp)
def update_flow(self, f):
super(ConsoleState, self).update_flow(f)
if self.focus is None:
self.set_focus(0)
return f

View File

@ -2,6 +2,7 @@
This module provides more sophisticated flow tracking and provides filtering and interception facilities.
"""
from __future__ import absolute_import
from abc import abstractmethod, ABCMeta
import hashlib
import Cookie
import cookielib
@ -338,80 +339,216 @@ class StickyAuthState:
f.request.headers["authorization"] = self.hosts[host]
class State(object):
def __init__(self):
self._flow_list = []
self.view = []
class FlowList(object):
__metaclass__ = ABCMeta
# These are compiled filt expressions:
self._limit = None
self.intercept = None
def __iter__(self):
return iter(self._list)
@property
def limit_txt(self):
if self._limit:
return self._limit.pattern
else:
return None
def __contains__(self, item):
return item in self._list
def flow_count(self):
return len(self._flow_list)
def __getitem__(self, item):
return self._list[item]
def __nonzero__(self):
return bool(self._list)
def __len__(self):
return len(self._list)
def index(self, f):
return self._flow_list.index(f)
return self._list.index(f)
def active_flow_count(self):
@abstractmethod
def _add(self, f):
return
@abstractmethod
def _update(self, f):
return
@abstractmethod
def _remove(self, f):
return
class FlowView(FlowList):
def __init__(self, store, filt=None):
self._list = []
if not filt:
filt = lambda flow: True
self._build(store, filt)
self.store = store
self.store.views.append(self)
def _close(self):
self.store.views.remove(self)
def _build(self, flows, filt=None):
if filt:
self.filt = filt
self._list = list(filter(self.filt, flows))
def _add(self, f):
if self.filt(f):
self._list.append(f)
def _update(self, f):
if f not in self._list:
self._add(f)
elif not self.filt(f):
self._remove(f)
def _remove(self, f):
if f in self._list:
self._list.remove(f)
def _recalculate(self, flows):
self._build(flows)
class FlowStore(FlowList):
"""
Responsible for handling flows in the state:
Keeps a list of all flows and provides views on them.
"""
def __init__(self):
self._list = []
self._set = set() # Used for O(1) lookups
self.views = []
self._recalculate_views()
def __contains__(self, f):
return f in self._set
def _add(self, f):
"""
Adds a flow to the state.
The flow to add must not be present in the state.
"""
self._list.append(f)
self._set.add(f)
for view in self.views:
view._add(f)
def _update(self, f):
"""
Notifies the state that a flow has been updated.
The flow must be present in the state.
"""
for view in self.views:
view._update(f)
def _remove(self, f):
"""
Deletes a flow from the state.
The flow must be present in the state.
"""
self._list.remove(f)
self._set.remove(f)
for view in self.views:
view._remove(f)
# Expensive bulk operations
def _extend(self, flows):
"""
Adds a list of flows to the state.
The list of flows to add must not contain flows that are already in the state.
"""
self._list.extend(flows)
self._set.update(flows)
self._recalculate_views()
def _clear(self):
self._list = []
self._set = set()
self._recalculate_views()
def _recalculate_views(self):
"""
Expensive operation: Recalculate all the views after a bulk change.
"""
for view in self.views:
view._recalculate(self)
# Utility functions.
# There are some common cases where we need to argue about all flows
# irrespective of filters on the view etc (i.e. on shutdown).
def active_count(self):
c = 0
for i in self._flow_list:
for i in self._list:
if not i.response and not i.error:
c += 1
return c
def add_request(self, flow):
"""
Add a request to the state. Returns the matching flow.
"""
if flow in self._flow_list: # catch flow replay
return flow
self._flow_list.append(flow)
if flow.match(self._limit):
self.view.append(flow)
return flow
# TODO: Should accept_all operate on views or on all flows?
def accept_all(self):
for f in self._list:
f.accept_intercept()
def add_response(self, f):
def kill_all(self, master):
for f in self._list:
f.kill(master)
class State(object):
def __init__(self):
self.flows = FlowStore()
self.view = FlowView(self.flows, None)
# These are compiled filt expressions:
self.intercept = None
@property
def limit_txt(self):
return getattr(self.view.filt, "pattern", None)
def flow_count(self):
return len(self.flows)
# TODO: All functions regarding flows that don't cause side-effects should be moved into FlowStore.
def index(self, f):
return self.flows.index(f)
def active_flow_count(self):
return self.flows.active_count()
def add_flow(self, f):
"""
Add a response to the state. Returns the matching flow.
Add a request to the state.
"""
if not f:
return False
if f.match(self._limit) and not f in self.view:
self.view.append(f)
self.flows._add(f)
return f
def add_error(self, f):
def update_flow(self, f):
"""
Add an error response to the state. Returns the matching flow, or
None if there isn't one.
Add a response to the state.
"""
if not f:
return None
if f.match(self._limit) and not f in self.view:
self.view.append(f)
self.flows._update(f)
return f
def delete_flow(self, f):
self.flows._remove(f)
def load_flows(self, flows):
self._flow_list.extend(flows)
self.recalculate_view()
self.flows._extend(flows)
def set_limit(self, txt):
if txt == self.limit_txt:
return
if txt:
f = filt.parse(txt)
if not f:
return "Invalid filter expression."
self._limit = f
self.view._close()
self.view = FlowView(self.flows, f)
else:
self._limit = None
self.recalculate_view()
self.view._close()
self.view = FlowView(self.flows, None)
def set_intercept(self, txt):
if txt:
@ -419,37 +556,24 @@ class State(object):
if not f:
return "Invalid filter expression."
self.intercept = f
self.intercept_txt = txt
else:
self.intercept = None
self.intercept_txt = None
def recalculate_view(self):
if self._limit:
self.view = [i for i in self._flow_list if i.match(self._limit)]
else:
self.view = self._flow_list[:]
def delete_flow(self, f):
self._flow_list.remove(f)
if f in self.view:
self.view.remove(f)
return True
@property
def intercept_txt(self):
return getattr(self.intercept, "pattern", None)
def clear(self):
for i in self._flow_list[:]:
self.delete_flow(i)
self.flows._clear()
def accept_all(self):
for i in self._flow_list[:]:
i.accept_intercept()
self.flows.accept_all()
def revert(self, f):
f.revert()
def killall(self, master):
for i in self._flow_list:
i.kill(master)
self.flows.kill_all(master)
class FlowMaster(controller.Master):
@ -716,7 +840,7 @@ class FlowMaster(controller.Master):
sc.reply()
def handle_error(self, f):
self.state.add_error(f)
self.state.update_flow(f)
self.run_script_hook("error", f)
if self.client_playback:
self.client_playback.clear(f)
@ -736,7 +860,8 @@ class FlowMaster(controller.Master):
self.add_event("Error in wsgi app. %s"%err, "error")
f.reply(protocol.KILL)
return
self.state.add_request(f)
if f not in self.state.flows: # don't add again on replay
self.state.add_flow(f)
self.replacehooks.run(f)
self.setheaders.run(f)
self.run_script_hook("request", f)
@ -757,7 +882,7 @@ class FlowMaster(controller.Master):
return f
def handle_response(self, f):
self.state.add_response(f)
self.state.update_flow(f)
self.replacehooks.run(f)
self.setheaders.run(f)
self.run_script_hook("response", f)
@ -772,7 +897,7 @@ class FlowMaster(controller.Master):
self.unload_scripts()
controller.Master.shutdown(self)
if self.stream:
for i in self.state._flow_list:
for i in self.state.flows:
if not i.response:
self.stream.add(i)
self.stop_stream()

View File

@ -117,7 +117,10 @@ class HTTPMessage(stateobject.StateObject):
def get_state(self, short=False):
ret = super(HTTPMessage, self).get_state(short)
if short:
ret["contentLength"] = len(self.content)
if self.content:
ret["contentLength"] = len(self.content)
else:
ret["contentLength"] = 0
return ret
def get_decoded_content(self):

View File

@ -1,4 +1,5 @@
from __future__ import absolute_import, print_function
import collections
import tornado.ioloop
import tornado.httpserver
from .. import controller, flow
@ -9,10 +10,64 @@ class Stop(Exception):
pass
class WebFlowView(flow.FlowView):
def __init__(self, store):
super(WebFlowView, self).__init__(store, None)
def _add(self, f):
super(WebFlowView, self)._add(f)
app.ClientConnection.broadcast(
type="flows",
cmd="add",
data=f.get_state(short=True)
)
def _update(self, f):
super(WebFlowView, self)._update(f)
app.ClientConnection.broadcast(
type="flows",
cmd="update",
data=f.get_state(short=True)
)
def _remove(self, f):
super(WebFlowView, self)._remove(f)
app.ClientConnection.broadcast(
type="flows",
cmd="remove",
data=f.get_state(short=True)
)
def _recalculate(self, flows):
super(WebFlowView, self)._recalculate(flows)
app.ClientConnection.broadcast(
type="flows",
cmd="reset"
)
class WebState(flow.State):
def __init__(self):
flow.State.__init__(self)
super(WebState, self).__init__()
self.view._close()
self.view = WebFlowView(self.flows)
self._last_event_id = 0
self.events = collections.deque(maxlen=1000)
def add_event(self, e, level):
self._last_event_id += 1
entry = {
"id": self._last_event_id,
"message": e,
"level": level
}
self.events.append(entry)
app.ClientConnection.broadcast(
type="events",
cmd="add",
data=entry
)
class Options(object):
attributes = [
@ -58,10 +113,8 @@ class Options(object):
class WebMaster(flow.FlowMaster):
def __init__(self, server, options):
self.options = options
self.app = app.Application(self.options.wdebug)
super(WebMaster, self).__init__(server, WebState())
self.last_log_id = 0
self.app = app.Application(self.state, self.options.wdebug)
def tick(self):
flow.FlowMaster.tick(self, self.masterq, timeout=0)
@ -83,33 +136,17 @@ class WebMaster(flow.FlowMaster):
self.shutdown()
def handle_request(self, f):
app.ClientConnection.broadcast("add_flow", f.get_state(True))
flow.FlowMaster.handle_request(self, f)
super(WebMaster, self).handle_request(f)
if f:
f.reply()
return f
def handle_response(self, f):
app.ClientConnection.broadcast("update_flow", f.get_state(True))
flow.FlowMaster.handle_response(self, f)
super(WebMaster, self).handle_response(f)
if f:
f.reply()
return f
def handle_error(self, f):
app.ClientConnection.broadcast("update_flow", f.get_state(True))
flow.FlowMaster.handle_error(self, f)
return f
def handle_log(self, l):
self.last_log_id += 1
app.ClientConnection.broadcast(
"add_event", {
"id": self.last_log_id,
"message": l.msg,
"level": l.level
}
)
self.add_event(l.msg, l.level)
l.reply()
def add_event(self, e, level="info"):
super(WebMaster, self).add_event(e, level)
self.state.add_event(e, level)

View File

@ -1,51 +1,84 @@
import os.path
import sys
import tornado.web
import tornado.websocket
import logging
import json
from .. import flow
class IndexHandler(tornado.web.RequestHandler):
def get(self):
_ = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645
self.render("index.html")
class ClientConnection(tornado.websocket.WebSocketHandler):
connections = set()
class WebSocketEventBroadcaster(tornado.websocket.WebSocketHandler):
connections = None # raise an error if inherited class doesn't specify its own instance.
def open(self):
ClientConnection.connections.add(self)
self.connections.add(self)
def on_close(self):
ClientConnection.connections.remove(self)
self.connections.remove(self)
@classmethod
def broadcast(cls, type, data):
def broadcast(cls, **kwargs):
message = json.dumps(kwargs)
for conn in cls.connections:
try:
conn.write_message(
json.dumps(
{
"type": type,
"data": data
}
)
)
conn.write_message(message)
except:
logging.error("Error sending message", exc_info=True)
class Flows(tornado.web.RequestHandler):
def get(self):
self.write(dict(
data=[f.get_state(short=True) for f in self.application.state.flows]
))
class Events(tornado.web.RequestHandler):
def get(self):
self.write(dict(
data=list(self.application.state.events)
))
class Settings(tornado.web.RequestHandler):
def get(self):
self.write(dict(
data=dict(
showEventLog=True
)
))
class FlowClear(tornado.web.RequestHandler):
def post(self):
self.application.state.clear()
class ClientConnection(WebSocketEventBroadcaster):
connections = set()
class Application(tornado.web.Application):
def __init__(self, debug):
def __init__(self, state, debug):
self.state = state
handlers = [
(r"/", IndexHandler),
(r"/updates", ClientConnection),
(r"/events", Events),
(r"/flows", Flows),
(r"/settings", Settings),
(r"/flows/clear", FlowClear),
]
settings = dict(
template_path=os.path.join(os.path.dirname(__file__), "templates"),
static_path=os.path.join(os.path.dirname(__file__), "static"),
xsrf_cookies=True,
cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
cookie_secret=os.urandom(256),
debug=debug,
)
tornado.web.Application.__init__(self, handlers, **settings)

View File

@ -270,7 +270,7 @@ header .menu {
margin-left: 3px;
}
footer {
box-shadow: 0 -1px 3px #d3d3d3;
box-shadow: 0 -1px 3px lightgray;
padding: 0px 10px 3px;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,7 @@ class TestConsoleState:
"""
c = console.ConsoleState()
f = self._add_request(c)
assert f in c._flow_list
assert f in c.flows
assert c.get_focus() == (f, 0)
def test_focus(self):
@ -52,19 +52,19 @@ class TestConsoleState:
def _add_request(self, state):
f = tutils.tflow()
return state.add_request(f)
return state.add_flow(f)
def _add_response(self, state):
f = self._add_request(state)
f.response = tutils.tresp()
state.add_response(f)
state.update_flow(f)
def test_add_response(self):
c = console.ConsoleState()
f = self._add_request(c)
f.response = tutils.tresp()
c.focus = None
c.add_response(f)
c.update_flow(f)
def test_focus_view(self):
c = console.ConsoleState()

View File

@ -364,7 +364,7 @@ class TestState:
def test_backup(self):
c = flow.State()
f = tutils.tflow()
c.add_request(f)
c.add_flow(f)
f.backup()
c.revert(f)
@ -376,42 +376,42 @@ class TestState:
"""
c = flow.State()
f = tutils.tflow()
c.add_request(f)
c.add_flow(f)
assert f
assert c.flow_count() == 1
assert c.active_flow_count() == 1
newf = tutils.tflow()
assert c.add_request(newf)
assert c.add_flow(newf)
assert c.active_flow_count() == 2
f.response = tutils.tresp()
assert c.add_response(f)
assert c.update_flow(f)
assert c.flow_count() == 2
assert c.active_flow_count() == 1
_ = tutils.tresp()
assert not c.add_response(None)
assert not c.update_flow(None)
assert c.active_flow_count() == 1
newf.response = tutils.tresp()
assert c.add_response(newf)
assert c.update_flow(newf)
assert c.active_flow_count() == 0
def test_err(self):
c = flow.State()
f = tutils.tflow()
c.add_request(f)
c.add_flow(f)
f.error = Error("message")
assert c.add_error(f)
assert c.update_flow(f)
c = flow.State()
f = tutils.tflow()
c.add_request(f)
c.add_flow(f)
c.set_limit("~e")
assert not c.view
f.error = tutils.terr()
assert c.add_error(f)
assert c.update_flow(f)
assert c.view
def test_set_limit(self):
@ -420,20 +420,20 @@ class TestState:
f = tutils.tflow()
assert len(c.view) == 0
c.add_request(f)
c.add_flow(f)
assert len(c.view) == 1
c.set_limit("~s")
assert c.limit_txt == "~s"
assert len(c.view) == 0
f.response = tutils.tresp()
c.add_response(f)
c.update_flow(f)
assert len(c.view) == 1
c.set_limit(None)
assert len(c.view) == 1
f = tutils.tflow()
c.add_request(f)
c.add_flow(f)
assert len(c.view) == 2
c.set_limit("~q")
assert len(c.view) == 1
@ -452,18 +452,18 @@ class TestState:
def _add_request(self, state):
f = tutils.tflow()
state.add_request(f)
state.add_flow(f)
return f
def _add_response(self, state):
f = tutils.tflow()
state.add_request(f)
state.add_flow(f)
f.response = tutils.tresp()
state.add_response(f)
state.update_flow(f)
def _add_error(self, state):
f = tutils.tflow(err=True)
state.add_request(f)
state.add_flow(f)
def test_clear(self):
c = flow.State()
@ -487,7 +487,7 @@ class TestState:
c.clear()
c.load_flows(flows)
assert isinstance(c._flow_list[0], Flow)
assert isinstance(c.flows[0], Flow)
def test_accept_all(self):
c = flow.State()
@ -532,7 +532,7 @@ class TestSerialize:
s = flow.State()
fm = flow.FlowMaster(None, s)
fm.load_flows(r)
assert len(s._flow_list) == 6
assert len(s.flows) == 6
def test_load_flows_reverse(self):
r = self._treader()
@ -540,7 +540,7 @@ class TestSerialize:
conf = ProxyConfig(mode="reverse", upstream_server=[True,True,"use-this-domain",80])
fm = flow.FlowMaster(DummyServer(conf), s)
fm.load_flows(r)
assert s._flow_list[0].request.host == "use-this-domain"
assert s.flows[0].request.host == "use-this-domain"
def test_filter(self):
sio = StringIO()

View File

@ -747,19 +747,19 @@ class TestProxyChainingSSLReconnect(tservers.HTTPUpstreamProxTest):
assert req.content == "content"
assert req.status_code == 418
assert not self.chain[1].tmaster.state._flow_list[0].response # killed
assert self.chain[1].tmaster.state._flow_list[1].response
assert not self.chain[1].tmaster.state.flows[0].response # killed
assert self.chain[1].tmaster.state.flows[1].response
assert self.proxy.tmaster.state._flow_list[0].request.form_in == "authority"
assert self.proxy.tmaster.state._flow_list[1].request.form_in == "relative"
assert self.proxy.tmaster.state.flows[0].request.form_in == "authority"
assert self.proxy.tmaster.state.flows[1].request.form_in == "relative"
assert self.chain[0].tmaster.state._flow_list[0].request.form_in == "authority"
assert self.chain[0].tmaster.state._flow_list[1].request.form_in == "relative"
assert self.chain[0].tmaster.state._flow_list[2].request.form_in == "authority"
assert self.chain[0].tmaster.state._flow_list[3].request.form_in == "relative"
assert self.chain[0].tmaster.state.flows[0].request.form_in == "authority"
assert self.chain[0].tmaster.state.flows[1].request.form_in == "relative"
assert self.chain[0].tmaster.state.flows[2].request.form_in == "authority"
assert self.chain[0].tmaster.state.flows[3].request.form_in == "relative"
assert self.chain[1].tmaster.state._flow_list[0].request.form_in == "relative"
assert self.chain[1].tmaster.state._flow_list[1].request.form_in == "relative"
assert self.chain[1].tmaster.state.flows[0].request.form_in == "relative"
assert self.chain[1].tmaster.state.flows[1].request.form_in == "relative"
req = p.request("get:'/p/418:b\"content2\"'")

View File

@ -10,8 +10,7 @@
"qunit": "",
"benchmark": "",
"benchmarkjs-runner": "",
"bootstrap": "",
"react-bootstrap": ""
"bootstrap": ""
},
"install": {
"path": "src/vendor",

View File

@ -30,19 +30,18 @@ var path = {
'vendor/lodash/lodash.js',
'vendor/react/react-with-addons.js',
'vendor/react-router/react-router.js',
'vendor/react-bootstrap/react-bootstrap.js'
],
app: [
'js/utils.js',
'js/dispatcher.js',
'js/actions.js',
'js/flow/utils.js',
'js/stores/base.js',
'js/stores/settingstore.js',
'js/stores/eventlogstore.js',
'js/stores/flowstore.js',
'js/store/store.js',
'js/store/view.js',
'js/store/settingstore.js',
'js/connection.js',
'js/components/utils.jsx.js',
'js/components/virtualscroll.jsx.js',
'js/components/header.jsx.js',
'js/components/flowtable-columns.jsx.js',
'js/components/flowtable.jsx.js',

View File

@ -3,24 +3,24 @@
"private": true,
"devDependencies": {
"bower": "",
"bower-installer": "^0.8.4",
"bower-installer": "",
"clean-css": "",
"gulp": "^3.8.8",
"gulp-concat": "^2.4.0",
"gulp-jshint": "^1.8.4",
"gulp-less": "^1.3.5",
"gulp-livereload": "^2.1.1",
"gulp-minify-css": "^0.3.8",
"gulp-notify": "^1.6.0",
"gulp-plumber": "^0.6.5",
"gulp-qunit": "^0.3.3",
"gulp-react": "^1.0.1",
"gulp-rename": "^1.2.0",
"gulp-sourcemaps": "^1.1.5",
"gulp-uglify": "^1.0.1",
"gulp-util": "^3.0.1",
"jshint-stylish": "^0.4.0",
"merge-stream": "^0.1.5",
"gulp": "",
"gulp-concat": "",
"gulp-jshint": "",
"gulp-less": "",
"gulp-livereload": "",
"gulp-minify-css": "",
"gulp-notify": "",
"gulp-plumber": "",
"gulp-qunit": "",
"gulp-react": "",
"gulp-rename": "",
"gulp-sourcemaps": "",
"gulp-uglify": "",
"gulp-util": "",
"jshint-stylish": "",
"merge-stream": "",
"react": "",
"react-tools": ""
}

File diff suppressed because it is too large Load Diff

View File

@ -1,37 +1,64 @@
var ActionTypes = {
//Settings
UPDATE_SETTINGS: "update_settings",
// Connection
CONNECTION_OPEN: "connection_open",
CONNECTION_CLOSE: "connection_close",
CONNECTION_ERROR: "connection_error",
//EventLog
ADD_EVENT: "add_event",
// Stores
SETTINGS_STORE: "settings",
EVENT_STORE: "events",
FLOW_STORE: "flows",
};
//Flow
ADD_FLOW: "add_flow",
UPDATE_FLOW: "update_flow",
var StoreCmds = {
ADD: "add",
UPDATE: "update",
REMOVE: "remove",
RESET: "reset"
};
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
});
}
};
var SettingsActions = {
update: function (settings) {
settings = _.merge({}, SettingsStore.getAll(), settings);
//TODO: Update server.
//Facebook Flux: We do an optimistic update on the client already.
AppDispatcher.dispatchViewAction({
type: ActionTypes.UPDATE_SETTINGS,
settings: settings
type: ActionTypes.SETTINGS_STORE,
cmd: StoreCmds.UPDATE,
data: settings
});
}
};
var event_id = 0;
var EventLogActions_event_id = 0;
var EventLogActions = {
add_event: function(message){
add_event: function (message) {
AppDispatcher.dispatchViewAction({
type: ActionTypes.ADD_EVENT,
type: ActionTypes.EVENT_STORE,
cmd: StoreCmds.ADD,
data: {
message: message,
level: "web",
id: "viewAction-"+event_id++
id: "viewAction-" + EventLogActions_event_id++
}
});
}

View File

@ -1,4 +1,7 @@
$(function () {
Connection.init();
app = React.renderComponent(ProxyApp, document.body);
window.ws = new Connection("/updates");
ReactRouter.run(routes, function (Handler) {
React.render(<Handler/>, document.body);
});
});

View File

@ -1,10 +1,8 @@
/** @jsx React.DOM */
var LogMessage = React.createClass({
render: function(){
render: function () {
var entry = this.props.entry;
var indicator;
switch(entry.level){
switch (entry.level) {
case "web":
indicator = <i className="fa fa-fw fa-html5"></i>;
break;
@ -20,47 +18,79 @@ var LogMessage = React.createClass({
</div>
);
},
shouldComponentUpdate: function(){
shouldComponentUpdate: function () {
return false; // log entries are immutable.
}
});
var EventLogContents = React.createClass({
mixins:[AutoScrollMixin],
mixins: [AutoScrollMixin, VirtualScrollMixin],
getInitialState: function () {
return {
log: []
};
},
componentDidMount: function () {
this.log = EventLogStore.getView();
this.log.addListener("change", this.onEventLogChange);
componentWillMount: function () {
this.openView(this.props.eventStore);
},
componentWillUnmount: function () {
this.log.removeListener("change", this.onEventLogChange);
this.log.close();
this.closeView();
},
openView: function (store) {
var view = new StoreView(store, function (entry) {
return this.props.filter[entry.level];
}.bind(this));
this.setState({
view: view
});
view.addListener("add recalculate", this.onEventLogChange);
},
closeView: function () {
this.state.view.close();
},
onEventLogChange: function () {
this.setState({
log: this.log.getAll()
log: this.state.view.list
});
},
componentWillReceiveProps: function (nextProps) {
if (nextProps.filter !== this.props.filter) {
this.props.filter = nextProps.filter; // Dirty: Make sure that view filter sees the update.
this.state.view.recalculate(this.props.eventStore.list);
}
if (nextProps.eventStore !== this.props.eventStore) {
this.closeView();
this.openView(nextProps.eventStore);
}
},
getDefaultProps: function () {
return {
rowHeight: 45,
rowHeightMin: 15,
placeholderTagName: "div"
};
},
renderRow: function (elem) {
return <LogMessage key={elem.id} entry={elem}/>;
},
render: function () {
var messages = this.state.log.map(function(row) {
if(!this.props.filter[row.level]){
return null;
}
return <LogMessage key={row.id} entry={row}/>;
}.bind(this));
return <pre>{messages}</pre>;
var rows = this.renderRows(this.state.log);
return <pre onScroll={this.onScroll}>
{ this.getPlaceholderTop(this.state.log.length) }
{rows}
{ this.getPlaceholderBottom(this.state.log.length) }
</pre>;
}
});
var ToggleFilter = React.createClass({
toggle: function(){
toggle: function (e) {
e.preventDefault();
return this.props.toggleLevel(this.props.name);
},
render: function(){
render: function () {
var className = "label ";
if (this.props.active) {
className += "label-primary";
@ -75,11 +105,11 @@ var ToggleFilter = React.createClass({
{this.props.name}
</a>
);
}
}
});
var EventLog = React.createClass({
getInitialState: function(){
getInitialState: function () {
return {
filter: {
"debug": false,
@ -93,11 +123,10 @@ var EventLog = React.createClass({
showEventLog: false
});
},
toggleLevel: function(level){
var filter = this.state.filter;
toggleLevel: function (level) {
var filter = _.extend({}, this.state.filter);
filter[level] = !filter[level];
this.setState({filter: filter});
return false;
},
render: function () {
return (
@ -112,7 +141,7 @@ var EventLog = React.createClass({
</div>
</div>
<EventLogContents filter={this.state.filter}/>
<EventLogContents filter={this.state.filter} eventStore={this.props.eventStore}/>
</div>
);
}

View File

@ -1,19 +1,17 @@
/** @jsx React.DOM */
var FlowDetailNav = React.createClass({
render: function(){
render: function () {
var items = this.props.tabs.map(function(e){
var items = this.props.tabs.map(function (e) {
var str = e.charAt(0).toUpperCase() + e.slice(1);
var className = this.props.active === e ? "active" : "";
var onClick = function(){
var onClick = function (event) {
this.props.selectTab(e);
return false;
event.preventDefault();
}.bind(this);
return <a key={e}
href="#"
className={className}
onClick={onClick}>{str}</a>;
href="#"
className={className}
onClick={onClick}>{str}</a>;
}.bind(this));
return (
<nav ref="head" className="nav-tabs nav-tabs-sm">
@ -24,11 +22,11 @@ var FlowDetailNav = React.createClass({
});
var Headers = React.createClass({
render: function(){
var rows = this.props.message.headers.map(function(header, i){
render: function () {
var rows = this.props.message.headers.map(function (header, i) {
return (
<tr key={i}>
<td className="header-name">{header[0]+":"}</td>
<td className="header-name">{header[0] + ":"}</td>
<td className="header-value">{header[1]}</td>
</tr>
);
@ -44,16 +42,16 @@ var Headers = React.createClass({
});
var FlowDetailRequest = React.createClass({
render: function(){
render: function () {
var flow = this.props.flow;
var first_line = [
flow.request.method,
RequestUtils.pretty_url(flow.request),
"HTTP/"+ flow.response.httpversion.join(".")
].join(" ");
flow.request.method,
RequestUtils.pretty_url(flow.request),
"HTTP/" + flow.request.httpversion.join(".")
].join(" ");
var content = null;
if(flow.request.contentLength > 0){
content = "Request Content Size: "+ formatSize(flow.request.contentLength);
if (flow.request.contentLength > 0) {
content = "Request Content Size: " + formatSize(flow.request.contentLength);
} else {
content = <div className="alert alert-info">No Content</div>;
}
@ -72,16 +70,16 @@ var FlowDetailRequest = React.createClass({
});
var FlowDetailResponse = React.createClass({
render: function(){
render: function () {
var flow = this.props.flow;
var first_line = [
"HTTP/"+ flow.response.httpversion.join("."),
flow.response.code,
flow.response.msg
].join(" ");
"HTTP/" + flow.response.httpversion.join("."),
flow.response.code,
flow.response.msg
].join(" ");
var content = null;
if(flow.response.contentLength > 0){
content = "Response Content Size: "+ formatSize(flow.response.contentLength);
if (flow.response.contentLength > 0) {
content = "Response Content Size: " + formatSize(flow.response.contentLength);
} else {
content = <div className="alert alert-info">No Content</div>;
}
@ -99,43 +97,67 @@ var FlowDetailResponse = React.createClass({
}
});
var TimeStamp = React.createClass({
render: function() {
var FlowDetailError = React.createClass({
render: function () {
var flow = this.props.flow;
return (
<section>
<div className="alert alert-warning">
{flow.error.msg}
<div><small>{ formatTimeStamp(flow.error.timestamp) }</small></div>
</div>
</section>
);
}
});
if(!this.props.t){
var TimeStamp = React.createClass({
render: function () {
if (!this.props.t) {
//should be return null, but that triggers a React bug.
return <tr></tr>;
}
var ts = (new Date(this.props.t * 1000)).toISOString();
ts = ts.replace("T", " ").replace("Z","");
var ts = formatTimeStamp(this.props.t);
var delta;
if(this.props.deltaTo){
delta = formatTimeDelta(1000 * (this.props.t-this.props.deltaTo));
if (this.props.deltaTo) {
delta = formatTimeDelta(1000 * (this.props.t - this.props.deltaTo));
delta = <span className="text-muted">{"(" + delta + ")"}</span>;
} else {
delta = null;
}
return <tr><td>{this.props.title + ":"}</td><td>{ts} {delta}</td></tr>;
return <tr>
<td>{this.props.title + ":"}</td>
<td>{ts} {delta}</td>
</tr>;
}
});
var ConnectionInfo = React.createClass({
render: function() {
render: function () {
var conn = this.props.conn;
var address = conn.address.address.join(":");
var sni = <tr key="sni"></tr>; //should be null, but that triggers a React bug.
if(conn.sni){
sni = <tr key="sni"><td><abbr title="TLS Server Name Indication">TLS SNI:</abbr></td><td>{conn.sni}</td></tr>;
if (conn.sni) {
sni = <tr key="sni">
<td>
<abbr title="TLS Server Name Indication">TLS SNI:</abbr>
</td>
<td>{conn.sni}</td>
</tr>;
}
return (
<table className="connection-table">
<tbody>
<tr key="address"><td>Address:</td><td>{address}</td></tr>
<tr key="address">
<td>Address:</td>
<td>{address}</td>
</tr>
{sni}
</tbody>
</table>
@ -144,7 +166,7 @@ var ConnectionInfo = React.createClass({
});
var CertificateInfo = React.createClass({
render: function(){
render: function () {
//TODO: We should fetch human-readable certificate representation
// from the server
var flow = this.props.flow;
@ -165,7 +187,7 @@ var CertificateInfo = React.createClass({
});
var Timing = React.createClass({
render: function(){
render: function () {
var flow = this.props.flow;
var sc = flow.server_conn;
var cc = flow.client_conn;
@ -218,82 +240,112 @@ var Timing = React.createClass({
}
//Add unique key for each row.
timestamps.forEach(function(e){
timestamps.forEach(function (e) {
e.key = e.title;
});
timestamps = _.sortBy(timestamps, 't');
var rows = timestamps.map(function(e){
return TimeStamp(e);
var rows = timestamps.map(function (e) {
return <TimeStamp {...e}/>;
});
return (
<div>
<h4>Timing</h4>
<table className="timing-table">
<tbody>
<h4>Timing</h4>
<table className="timing-table">
<tbody>
{rows}
</tbody>
</table>
</tbody>
</table>
</div>
);
}
});
var FlowDetailConnectionInfo = React.createClass({
render: function(){
render: function () {
var flow = this.props.flow;
var client_conn = flow.client_conn;
var server_conn = flow.server_conn;
return (
<section>
<h4>Client Connection</h4>
<ConnectionInfo conn={client_conn}/>
<h4>Client Connection</h4>
<ConnectionInfo conn={client_conn}/>
<h4>Server Connection</h4>
<ConnectionInfo conn={server_conn}/>
<h4>Server Connection</h4>
<ConnectionInfo conn={server_conn}/>
<CertificateInfo flow={flow}/>
<CertificateInfo flow={flow}/>
<Timing flow={flow}/>
<Timing flow={flow}/>
</section>
);
}
});
var tabs = {
var allTabs = {
request: FlowDetailRequest,
response: FlowDetailResponse,
error: FlowDetailError,
details: FlowDetailConnectionInfo
};
var FlowDetail = React.createClass({
getDefaultProps: function(){
return {
tabs: ["request","response", "details"]
};
mixins: [StickyHeadMixin, ReactRouter.Navigation, ReactRouter.State],
getTabs: function (flow) {
var tabs = [];
["request", "response", "error"].forEach(function (e) {
if (flow[e]) {
tabs.push(e);
}
});
tabs.push("details");
return tabs;
},
mixins: [StickyHeadMixin],
nextTab: function(i) {
var currentIndex = this.props.tabs.indexOf(this.props.active);
nextTab: function (i) {
var tabs = this.getTabs(this.props.flow);
var currentIndex = tabs.indexOf(this.getParams().detailTab);
// JS modulo operator doesn't correct negative numbers, make sure that we are positive.
var nextIndex = (currentIndex + i + this.props.tabs.length) % this.props.tabs.length;
this.props.selectTab(this.props.tabs[nextIndex]);
var nextIndex = (currentIndex + i + tabs.length) % tabs.length;
this.selectTab(tabs[nextIndex]);
},
render: function(){
var flow = JSON.stringify(this.props.flow, null, 2);
var Tab = tabs[this.props.active];
selectTab: function (panel) {
this.replaceWith(
"flow",
{
flowId: this.getParams().flowId,
detailTab: panel
}
);
},
render: function () {
var flow = this.props.flow;
var tabs = this.getTabs(flow);
var active = this.getParams().detailTab;
if (!_.contains(tabs, active)) {
if (active === "response" && flow.error) {
active = "error";
} else if (active === "error" && flow.response) {
active = "response";
} else {
active = tabs[0];
}
this.selectTab(active);
}
var Tab = allTabs[active];
return (
<div className="flow-detail" onScroll={this.adjustHead}>
<FlowDetailNav ref="head"
tabs={this.props.tabs}
active={this.props.active}
selectTab={this.props.selectTab}/>
<Tab flow={this.props.flow}/>
tabs={tabs}
active={active}
selectTab={this.selectTab}/>
<Tab flow={flow}/>
</div>
);
);
}
});

View File

@ -1,17 +1,14 @@
/** @jsx React.DOM */
var TLSColumn = React.createClass({
statics: {
renderTitle: function(){
renderTitle: function () {
return <th key="tls" className="col-tls"></th>;
}
},
render: function(){
render: function () {
var flow = this.props.flow;
var ssl = (flow.request.scheme == "https");
var classes;
if(ssl){
if (ssl) {
classes = "col-tls col-tls-https";
} else {
classes = "col-tls col-tls-http";
@ -23,23 +20,23 @@ var TLSColumn = React.createClass({
var IconColumn = React.createClass({
statics: {
renderTitle: function(){
renderTitle: function () {
return <th key="icon" className="col-icon"></th>;
}
},
render: function(){
render: function () {
var flow = this.props.flow;
var icon;
if(flow.response){
if (flow.response) {
var contentType = ResponseUtils.getContentType(flow.response);
//TODO: We should assign a type to the flow somewhere else.
if(flow.response.code == 304) {
if (flow.response.code == 304) {
icon = "resource-icon-not-modified";
} else if(300 <= flow.response.code && flow.response.code < 400) {
} else if (300 <= flow.response.code && flow.response.code < 400) {
icon = "resource-icon-redirect";
} else if(contentType && contentType.indexOf("image") >= 0) {
} else if (contentType && contentType.indexOf("image") >= 0) {
icon = "resource-icon-image";
} else if (contentType && contentType.indexOf("javascript") >= 0) {
icon = "resource-icon-js";
@ -49,23 +46,25 @@ var IconColumn = React.createClass({
icon = "resource-icon-document";
}
}
if(!icon){
if (!icon) {
icon = "resource-icon-plain";
}
icon += " resource-icon";
return <td className="col-icon"><div className={icon}></div></td>;
return <td className="col-icon">
<div className={icon}></div>
</td>;
}
});
var PathColumn = React.createClass({
statics: {
renderTitle: function(){
renderTitle: function () {
return <th key="path" className="col-path">Path</th>;
}
},
render: function(){
render: function () {
var flow = this.props.flow;
return <td className="col-path">{flow.request.scheme + "://" + flow.request.host + flow.request.path}</td>;
}
@ -74,11 +73,11 @@ var PathColumn = React.createClass({
var MethodColumn = React.createClass({
statics: {
renderTitle: function(){
renderTitle: function () {
return <th key="method" className="col-method">Method</th>;
}
},
render: function(){
render: function () {
var flow = this.props.flow;
return <td className="col-method">{flow.request.method}</td>;
}
@ -87,14 +86,14 @@ var MethodColumn = React.createClass({
var StatusColumn = React.createClass({
statics: {
renderTitle: function(){
renderTitle: function () {
return <th key="status" className="col-status">Status</th>;
}
},
render: function(){
render: function () {
var flow = this.props.flow;
var status;
if(flow.response){
if (flow.response) {
status = flow.response.code;
} else {
status = null;
@ -106,15 +105,15 @@ var StatusColumn = React.createClass({
var SizeColumn = React.createClass({
statics: {
renderTitle: function(){
renderTitle: function () {
return <th key="size" className="col-size">Size</th>;
}
},
render: function(){
render: function () {
var flow = this.props.flow;
var total = flow.request.contentLength;
if(flow.response){
if (flow.response) {
total += flow.response.contentLength || 0;
}
var size = formatSize(total);
@ -125,14 +124,14 @@ var SizeColumn = React.createClass({
var TimeColumn = React.createClass({
statics: {
renderTitle: function(){
renderTitle: function () {
return <th key="time" className="col-time">Time</th>;
}
},
render: function(){
render: function () {
var flow = this.props.flow;
var time;
if(flow.response){
if (flow.response) {
time = formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start));
} else {
time = "...";

View File

@ -1,13 +1,11 @@
/** @jsx React.DOM */
var FlowRow = React.createClass({
render: function(){
render: function () {
var flow = this.props.flow;
var columns = this.props.columns.map(function(column){
return <column key={column.displayName} flow={flow}/>;
var columns = this.props.columns.map(function (Column) {
return <Column key={Column.displayName} flow={flow}/>;
}.bind(this));
var className = "";
if(this.props.selected){
if (this.props.selected) {
className += "selected";
}
return (
@ -15,80 +13,97 @@ var FlowRow = React.createClass({
{columns}
</tr>);
},
shouldComponentUpdate: function(nextProps){
var isEqual = (
this.props.columns.length === nextProps.columns.length &&
this.props.selected === nextProps.selected &&
this.props.flow.response === nextProps.flow.response);
return !isEqual;
shouldComponentUpdate: function (nextProps) {
return true;
// Further optimization could be done here
// by calling forceUpdate on flow updates, selection changes and column changes.
//return (
//(this.props.columns.length !== nextProps.columns.length) ||
//(this.props.selected !== nextProps.selected)
//);
}
});
var FlowTableHead = React.createClass({
render: function(){
var columns = this.props.columns.map(function(column){
render: function () {
var columns = this.props.columns.map(function (column) {
return column.renderTitle();
}.bind(this));
return <thead><tr>{columns}</tr></thead>;
return <thead>
<tr>{columns}</tr>
</thead>;
}
});
var FlowTableBody = React.createClass({
render: function(){
var rows = this.props.flows.map(function(flow){
var selected = (flow == this.props.selected);
return <FlowRow key={flow.id}
ref={flow.id}
flow={flow}
columns={this.props.columns}
selected={selected}
selectFlow={this.props.selectFlow}
/>;
}.bind(this));
return <tbody>{rows}</tbody>;
}
});
var ROW_HEIGHT = 32;
var FlowTable = React.createClass({
mixins: [StickyHeadMixin, AutoScrollMixin],
mixins: [StickyHeadMixin, AutoScrollMixin, VirtualScrollMixin],
getInitialState: function () {
return {
columns: all_columns
};
},
scrollIntoView: function(flow){
// Now comes the fun part: Scroll the flow into the view.
var viewport = this.getDOMNode();
var flowNode = this.refs.body.refs[flow.id].getDOMNode();
var viewport_top = viewport.scrollTop;
var viewport_bottom = viewport_top + viewport.offsetHeight;
var flowNode_top = flowNode.offsetTop;
var flowNode_bottom = flowNode_top + flowNode.offsetHeight;
// Account for pinned thead by pretending that the flowNode starts
// -thead_height pixel earlier.
flowNode_top -= this.refs.body.getDOMNode().offsetTop;
if(flowNode_top < viewport_top){
viewport.scrollTop = flowNode_top;
} else if(flowNode_bottom > viewport_bottom) {
viewport.scrollTop = flowNode_bottom - viewport.offsetHeight;
componentWillMount: function () {
if (this.props.view) {
this.props.view.addListener("add update remove recalculate", this.onChange);
}
},
componentWillReceiveProps: function (nextProps) {
if (nextProps.view !== this.props.view) {
if (this.props.view) {
this.props.view.removeListener("add update remove recalculate");
}
nextProps.view.addListener("add update remove recalculate", this.onChange);
}
},
getDefaultProps: function () {
return {
rowHeight: ROW_HEIGHT
};
},
onScrollFlowTable: function () {
this.adjustHead();
this.onScroll();
},
onChange: function () {
this.forceUpdate();
},
scrollIntoView: function (flow) {
this.scrollRowIntoView(
this.props.view.index(flow),
this.refs.body.getDOMNode().offsetTop
);
},
renderRow: function (flow) {
var selected = (flow === this.props.selected);
return <FlowRow key={flow.id}
ref={flow.id}
flow={flow}
columns={this.state.columns}
selected={selected}
selectFlow={this.props.selectFlow}
/>;
},
render: function () {
//console.log("render flowtable", this.state.start, this.state.stop, this.props.selected);
var flows = this.props.view ? this.props.view.list : [];
var rows = this.renderRows(flows);
return (
<div className="flow-table" onScroll={this.adjustHead}>
<div className="flow-table" onScroll={this.onScrollFlowTable}>
<table>
<FlowTableHead ref="head"
columns={this.state.columns}/>
<FlowTableBody ref="body"
flows={this.props.flows}
selected={this.props.selected}
selectFlow={this.props.selectFlow}
columns={this.state.columns}/>
columns={this.state.columns}/>
<tbody ref="body">
{ this.getPlaceholderTop(flows.length) }
{rows}
{ this.getPlaceholderBottom(flows.length) }
</tbody>
</table>
</div>
);
);
}
});

View File

@ -1,5 +1,3 @@
/** @jsx React.DOM */
var Footer = React.createClass({
render: function () {
var mode = this.props.settings.mode;
@ -7,6 +5,6 @@ var Footer = React.createClass({
<footer>
{mode != "regular" ? <span className="label label-success">{mode} mode</span> : null}
</footer>
);
);
}
});

View File

@ -1,5 +1,3 @@
/** @jsx React.DOM */
var MainMenu = React.createClass({
statics: {
title: "Traffic",
@ -10,14 +8,23 @@ var MainMenu = React.createClass({
showEventLog: !this.props.settings.showEventLog
});
},
clearFlows: function () {
$.post("/flows/clear");
},
render: function () {
return (
<div>
<button className={"btn " + (this.props.settings.showEventLog ? "btn-primary" : "btn-default")} onClick={this.toggleEventLog}>
<i className="fa fa-database"></i> Display Event Log
<i className="fa fa-database"></i>
&nbsp;Display Event Log
</button>
&nbsp;
<button className="btn btn-default" onClick={this.clearFlows}>
<i className="fa fa-eraser"></i>
&nbsp;Clear Flows
</button>
</div>
);
);
}
});
@ -43,38 +50,110 @@ var ReportsMenu = React.createClass({
}
});
var FileMenu = React.createClass({
getInitialState: function () {
return {
showFileMenu: false
};
},
handleFileClick: function (e) {
e.preventDefault();
if (!this.state.showFileMenu) {
var close = function () {
this.setState({showFileMenu: false});
document.removeEventListener("click", close);
}.bind(this);
document.addEventListener("click", close);
this.setState({
showFileMenu: true
});
}
},
handleNewClick: function(e){
e.preventDefault();
console.error("unimplemented: handleNewClick");
},
handleOpenClick: function(e){
e.preventDefault();
console.error("unimplemented: handleOpenClick");
},
handleSaveClick: function(e){
e.preventDefault();
console.error("unimplemented: handleSaveClick");
},
handleShutdownClick: function(e){
e.preventDefault();
console.error("unimplemented: handleShutdownClick");
},
render: function () {
var fileMenuClass = "dropdown pull-left" + (this.state.showFileMenu ? " open" : "");
return (
<div className={fileMenuClass}>
<a href="#" className="special" onClick={this.handleFileClick}> File </a>
<ul className="dropdown-menu" role="menu">
<li>
<a href="#" onClick={this.handleNewClick}>
<i className="fa fa-fw fa-file"></i>
New
</a>
</li>
<li>
<a href="#" onClick={this.handleOpenClick}>
<i className="fa fa-fw fa-folder-open"></i>
Open
</a>
</li>
<li>
<a href="#" onClick={this.handleSaveClick}>
<i className="fa fa-fw fa-save"></i>
Save
</a>
</li>
<li role="presentation" className="divider"></li>
<li>
<a href="#" onClick={this.handleShutdownClick}>
<i className="fa fa-fw fa-plug"></i>
Shutdown
</a>
</li>
</ul>
</div>
);
}
});
var header_entries = [MainMenu, ToolsMenu, ReportsMenu];
var Header = React.createClass({
mixins: [ReactRouter.Navigation],
getInitialState: function () {
return {
active: header_entries[0]
};
},
handleClick: function (active) {
ReactRouter.transitionTo(active.route);
handleClick: function (active, e) {
e.preventDefault();
this.transitionTo(active.route);
this.setState({active: active});
return false;
},
handleFileClick: function () {
console.log("File click");
},
render: function () {
var header = header_entries.map(function(entry, i){
var header = header_entries.map(function (entry, i) {
var classes = React.addons.classSet({
active: entry == this.state.active
});
return (
<a key={i}
href="#"
className={classes}
onClick={this.handleClick.bind(this, entry)}
href="#"
className={classes}
onClick={this.handleClick.bind(this, entry)}
>
{ entry.title}
</a>
);
);
}.bind(this));
return (
@ -83,13 +162,13 @@ var Header = React.createClass({
mitmproxy { this.props.settings.version }
</div>
<nav className="nav-tabs nav-tabs-lg">
<a href="#" className="special" onClick={this.handleFileClick}> File </a>
<FileMenu/>
{header}
</nav>
<div className="menu">
<this.state.active settings={this.props.settings}/>
</div>
</header>
);
);
}
});

View File

@ -1,65 +1,86 @@
/** @jsx React.DOM */
var MainView = React.createClass({
getInitialState: function() {
mixins: [ReactRouter.Navigation, ReactRouter.State],
getInitialState: function () {
return {
flows: [],
flows: []
};
},
componentDidMount: function () {
this.flowStore = FlowStore.getView();
this.flowStore.addListener("change",this.onFlowChange);
componentWillReceiveProps: function (nextProps) {
if (nextProps.flowStore !== this.props.flowStore) {
this.closeView();
this.openView(nextProps.flowStore);
}
},
openView: function (store) {
var view = new StoreView(store);
this.setState({
view: view
});
view.addListener("recalculate", this.onRecalculate);
view.addListener("add update remove", this.onUpdate);
},
onRecalculate: function(){
this.forceUpdate();
var selected = this.getSelected();
if(selected){
this.refs.flowTable.scrollIntoView(selected);
}
},
onUpdate: function (flow) {
if (flow.id === this.getParams().flowId) {
this.forceUpdate();
}
},
closeView: function () {
this.state.view.close();
},
componentWillMount: function () {
this.openView(this.props.flowStore);
},
componentWillUnmount: function () {
this.flowStore.removeListener("change",this.onFlowChange);
this.flowStore.close();
this.closeView();
},
onFlowChange: function () {
this.setState({
flows: this.flowStore.getAll()
});
},
selectDetailTab: function(panel) {
ReactRouter.replaceWith(
"flow",
{
flowId: this.props.params.flowId,
detailTab: panel
}
);
},
selectFlow: function(flow) {
if(flow){
ReactRouter.replaceWith(
selectFlow: function (flow) {
if (flow) {
this.replaceWith(
"flow",
{
flowId: flow.id,
detailTab: this.props.params.detailTab || "request"
detailTab: this.getParams().detailTab || "request"
}
);
this.refs.flowTable.scrollIntoView(flow);
} else {
ReactRouter.replaceWith("flows");
this.replaceWith("flows");
}
},
selectFlowRelative: function(i){
selectFlowRelative: function (shift) {
var flows = this.state.view.list;
var index;
if(!this.props.params.flowId){
if(i > 0){
index = this.state.flows.length-1;
if (!this.getParams().flowId) {
if (shift > 0) {
index = flows.length - 1;
} else {
index = 0;
}
} else {
index = _.findIndex(this.state.flows, function(f){
return f.id === this.props.params.flowId;
}.bind(this));
index = Math.min(Math.max(0, index+i), this.state.flows.length-1);
var currFlowId = this.getParams().flowId;
var i = flows.length;
while (i--) {
if (flows[i].id === currFlowId) {
index = i;
break;
}
}
index = Math.min(
Math.max(0, index + shift),
flows.length - 1);
}
this.selectFlow(this.state.flows[index]);
this.selectFlow(flows[index]);
},
onKeyDown: function(e){
switch(e.keyCode){
onKeyDown: function (e) {
switch (e.keyCode) {
case Key.K:
case Key.UP:
this.selectFlowRelative(-1);
@ -80,14 +101,14 @@ var MainView = React.createClass({
break;
case Key.H:
case Key.LEFT:
if(this.refs.flowDetails){
if (this.refs.flowDetails) {
this.refs.flowDetails.nextTab(-1);
}
break;
case Key.L:
case Key.TAB:
case Key.RIGHT:
if(this.refs.flowDetails){
if (this.refs.flowDetails) {
this.refs.flowDetails.nextTab(+1);
}
break;
@ -95,19 +116,20 @@ var MainView = React.createClass({
console.debug("keydown", e.keyCode);
return;
}
return false;
e.preventDefault();
},
render: function() {
var selected = _.find(this.state.flows, { id: this.props.params.flowId });
getSelected: function(){
return this.props.flowStore.get(this.getParams().flowId);
},
render: function () {
var selected = this.getSelected();
var details;
if(selected){
details = (
<FlowDetail ref="flowDetails"
flow={selected}
selectTab={this.selectDetailTab}
active={this.props.params.detailTab}/>
);
if (selected) {
details = [
<Splitter key="splitter"/>,
<FlowDetail key="flowDetails" ref="flowDetails" flow={selected}/>
];
} else {
details = null;
}
@ -115,10 +137,9 @@ var MainView = React.createClass({
return (
<div className="main-view" onKeyDown={this.onKeyDown} tabIndex="0">
<FlowTable ref="flowTable"
flows={this.state.flows}
selectFlow={this.selectFlow}
selected={selected} />
{ details ? <Splitter/> : null }
view={this.state.view}
selectFlow={this.selectFlow}
selected={selected} />
{details}
</div>
);

View File

@ -1,5 +1,3 @@
/** @jsx React.DOM */
//TODO: Move out of here, just a stub.
var Reports = React.createClass({
render: function () {
@ -10,45 +8,67 @@ var Reports = React.createClass({
var ProxyAppMain = React.createClass({
getInitialState: function () {
return { settings: SettingsStore.getAll() };
var eventStore = new EventLogStore();
var flowStore = new FlowStore();
var settings = new SettingsStore();
// Default Settings before fetch
_.extend(settings.dict,{
showEventLog: true
});
return {
settings: settings,
flowStore: flowStore,
eventStore: eventStore
};
},
componentDidMount: function () {
SettingsStore.addListener("change", this.onSettingsChange);
this.state.settings.addListener("recalculate", this.onSettingsChange);
},
componentWillUnmount: function () {
SettingsStore.removeListener("change", this.onSettingsChange);
this.state.settings.removeListener("recalculate", this.onSettingsChange);
},
onSettingsChange: function () {
this.setState({settings: SettingsStore.getAll()});
onSettingsChange: function(){
this.setState({
settings: this.state.settings
});
},
render: function () {
var eventlog;
if (this.state.settings.dict.showEventLog) {
eventlog = [
<Splitter key="splitter" axis="y"/>,
<EventLog key="eventlog" eventStore={this.state.eventStore}/>
];
} else {
eventlog = null;
}
return (
<div id="container">
<Header settings={this.state.settings}/>
<this.props.activeRouteHandler settings={this.state.settings}/>
{this.state.settings.showEventLog ? <Splitter axis="y"/> : null}
{this.state.settings.showEventLog ? <EventLog/> : null}
<Footer settings={this.state.settings}/>
<Header settings={this.state.settings.dict}/>
<RouteHandler settings={this.state.settings.dict} flowStore={this.state.flowStore}/>
{eventlog}
<Footer settings={this.state.settings.dict}/>
</div>
);
);
}
});
var Routes = ReactRouter.Routes;
var Route = ReactRouter.Route;
var RouteHandler = ReactRouter.RouteHandler;
var Redirect = ReactRouter.Redirect;
var DefaultRoute = ReactRouter.DefaultRoute;
var NotFoundRoute = ReactRouter.NotFoundRoute;
var ProxyApp = (
<Routes location="hash">
<Route path="/" handler={ProxyAppMain}>
<Route name="flows" path="flows" handler={MainView}/>
<Route name="flow" path="flows/:flowId/:detailTab" handler={MainView}/>
<Route name="reports" handler={Reports}/>
<Redirect path="/" to="flows" />
</Route>
</Routes>
);
var routes = (
<Route path="/" handler={ProxyAppMain}>
<Route name="flows" path="flows" handler={MainView}/>
<Route name="flow" path="flows/:flowId/:detailTab" handler={MainView}/>
<Route name="reports" handler={Reports}/>
<Redirect path="/" to="flows" />
</Route>
);

View File

@ -1,5 +1,3 @@
/** @jsx React.DOM */
//React utils. For other utilities, see ../utils.js
var Splitter = React.createClass({
@ -8,62 +6,70 @@ var Splitter = React.createClass({
axis: "x"
};
},
getInitialState: function(){
getInitialState: function () {
return {
applied: false,
startX: false,
startY: false
};
},
onMouseDown: function(e){
onMouseDown: function (e) {
this.setState({
startX: e.pageX,
startY: e.pageY
});
window.addEventListener("mousemove",this.onMouseMove);
window.addEventListener("mouseup",this.onMouseUp);
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
// Occasionally, only a dragEnd event is triggered, but no mouseUp.
window.addEventListener("dragend",this.onDragEnd);
window.addEventListener("dragend", this.onDragEnd);
},
onDragEnd: function(){
this.getDOMNode().style.transform="";
window.removeEventListener("dragend",this.onDragEnd);
window.removeEventListener("mouseup",this.onMouseUp);
window.removeEventListener("mousemove",this.onMouseMove);
onDragEnd: function () {
this.getDOMNode().style.transform = "";
window.removeEventListener("dragend", this.onDragEnd);
window.removeEventListener("mouseup", this.onMouseUp);
window.removeEventListener("mousemove", this.onMouseMove);
},
onMouseUp: function(e){
onMouseUp: function (e) {
this.onDragEnd();
var node = this.getDOMNode();
var prev = node.previousElementSibling;
var next = node.nextElementSibling;
var dX = e.pageX-this.state.startX;
var dY = e.pageY-this.state.startY;
var dX = e.pageX - this.state.startX;
var dY = e.pageY - this.state.startY;
var flexBasis;
if(this.props.axis === "x"){
if (this.props.axis === "x") {
flexBasis = prev.offsetWidth + dX;
} else {
flexBasis = prev.offsetHeight + dY;
}
prev.style.flex = "0 0 "+Math.max(0, flexBasis)+"px";
prev.style.flex = "0 0 " + Math.max(0, flexBasis) + "px";
next.style.flex = "1 1 auto";
this.setState({
applied: true
});
this.onResize();
},
onMouseMove: function(e){
onMouseMove: function (e) {
var dX = 0, dY = 0;
if(this.props.axis === "x"){
dX = e.pageX-this.state.startX;
if (this.props.axis === "x") {
dX = e.pageX - this.state.startX;
} else {
dY = e.pageY-this.state.startY;
dY = e.pageY - this.state.startY;
}
this.getDOMNode().style.transform = "translate("+dX+"px,"+dY+"px)";
this.getDOMNode().style.transform = "translate(" + dX + "px," + dY + "px)";
},
reset: function(willUnmount) {
onResize: function () {
// Trigger a global resize event. This notifies components that employ virtual scrolling
// that their viewport may have changed.
window.setTimeout(function () {
window.dispatchEvent(new CustomEvent("resize"));
}, 1);
},
reset: function (willUnmount) {
if (!this.state.applied) {
return;
}
@ -74,19 +80,19 @@ var Splitter = React.createClass({
prev.style.flex = "";
next.style.flex = "";
if(!willUnmount){
if (!willUnmount) {
this.setState({
applied: false
});
}
this.onResize();
},
componentWillUnmount: function(){
componentWillUnmount: function () {
this.reset(true);
},
render: function(){
render: function () {
var className = "splitter";
if(this.props.axis === "x"){
if (this.props.axis === "x") {
className += " splitter-x";
} else {
className += " splitter-y";
@ -98,3 +104,20 @@ var Splitter = React.createClass({
);
}
});
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
var xsrf = $.param({_xsrf: getCookie("_xsrf")});
//Tornado XSRF Protection.
$.ajaxPrefilter(function (options) {
if (options.type === "post" && options.url[0] === "/") {
if (options.data) {
options.data += ("&" + xsrf);
} else {
options.data = xsrf;
}
}
});

View File

@ -0,0 +1,81 @@
var VirtualScrollMixin = {
getInitialState: function () {
return {
start: 0,
stop: 0
};
},
componentWillMount: function () {
if (!this.props.rowHeight) {
console.warn("VirtualScrollMixin: No rowHeight specified", this);
}
},
getPlaceholderTop: function (total) {
var Tag = this.props.placeholderTagName || "tr";
// When a large trunk of elements is removed from the button, start may be far off the viewport.
// To make this issue less severe, limit the top placeholder to the total number of rows.
var style = {
height: Math.min(this.state.start, total) * this.props.rowHeight
};
var spacer = <Tag key="placeholder-top" style={style}></Tag>;
if (this.state.start % 2 === 1) {
// fix even/odd rows
return [spacer, <Tag key="placeholder-top-2"></Tag>];
} else {
return spacer;
}
},
getPlaceholderBottom: function (total) {
var Tag = this.props.placeholderTagName || "tr";
var style = {
height: Math.max(0, total - this.state.stop) * this.props.rowHeight
};
return <Tag key="placeholder-bottom" style={style}></Tag>;
},
componentDidMount: function () {
this.onScroll();
window.addEventListener('resize', this.onScroll);
},
componentWillUnmount: function(){
window.removeEventListener('resize', this.onScroll);
},
onScroll: function () {
var viewport = this.getDOMNode();
var top = viewport.scrollTop;
var height = viewport.offsetHeight;
var start = Math.floor(top / this.props.rowHeight);
var stop = start + Math.ceil(height / (this.props.rowHeightMin || this.props.rowHeight));
this.setState({
start: start,
stop: stop
});
},
renderRows: function (elems) {
var rows = [];
var max = Math.min(elems.length, this.state.stop);
for (var i = this.state.start; i < max; i++) {
var elem = elems[i];
rows.push(this.renderRow(elem));
}
return rows;
},
scrollRowIntoView: function (index, head_height) {
var row_top = (index * this.props.rowHeight) + head_height;
var row_bottom = row_top + this.props.rowHeight;
var viewport = this.getDOMNode();
var viewport_top = viewport.scrollTop;
var viewport_bottom = viewport_top + viewport.offsetHeight;
// Account for pinned thead
if (row_top - head_height < viewport_top) {
viewport.scrollTop = row_top - head_height;
} else if (row_bottom > viewport_bottom) {
viewport.scrollTop = row_bottom - viewport.offsetHeight;
}
},
};

View File

@ -1,33 +1,24 @@
function _Connection(url) {
this.url = url;
function Connection(url) {
if (url[0] === "/") {
url = location.origin.replace("http", "ws") + url;
}
var ws = new WebSocket(url);
ws.onopen = function () {
ConnectionActions.open();
};
ws.onmessage = function (message) {
var m = JSON.parse(message.data);
AppDispatcher.dispatchServerAction(m);
};
ws.onerror = function () {
ConnectionActions.error();
EventLogActions.add_event("WebSocket connection error.");
};
ws.onclose = function () {
ConnectionActions.close();
EventLogActions.add_event("WebSocket connection closed.");
};
return ws;
}
_Connection.prototype.init = function () {
this.openWebSocketConnection();
};
_Connection.prototype.openWebSocketConnection = function () {
this.ws = new WebSocket(this.url.replace("http", "ws"));
var ws = this.ws;
ws.onopen = this.onopen.bind(this);
ws.onmessage = this.onmessage.bind(this);
ws.onerror = this.onerror.bind(this);
ws.onclose = this.onclose.bind(this);
};
_Connection.prototype.onopen = function (open) {
console.debug("onopen", this, arguments);
};
_Connection.prototype.onmessage = function (message) {
//AppDispatcher.dispatchServerAction(...);
var m = JSON.parse(message.data);
AppDispatcher.dispatchServerAction(m);
};
_Connection.prototype.onerror = function (error) {
EventLogActions.add_event("WebSocket Connection Error.");
console.debug("onerror", this, arguments);
};
_Connection.prototype.onclose = function (close) {
EventLogActions.add_event("WebSocket Connection closed.");
console.debug("onclose", this, arguments);
};
var Connection = new _Connection(location.origin + "/updates");

View File

@ -11,14 +11,14 @@ Dispatcher.prototype.register = function (callback) {
this.callbacks.push(callback);
};
Dispatcher.prototype.unregister = function (callback) {
var index = this.callbacks.indexOf(f);
var index = this.callbacks.indexOf(callback);
if (index >= 0) {
this.callbacks.splice(this.callbacks.indexOf(f), 1);
this.callbacks.splice(index, 1);
}
};
Dispatcher.prototype.dispatch = function (payload) {
console.debug("dispatch", payload);
for(var i = 0; i < this.callbacks.length; i++){
for (var i = 0; i < this.callbacks.length; i++) {
this.callbacks[i](payload);
}
};

View File

@ -0,0 +1,93 @@
// PEG.js filter rules - see http://pegjs.majda.cz/online
/* Explain Filter */
{
var or = function(first, second) {
return first + " or " + second;
};
var and = function(first, second) {
return first + " and " + second;
};
var not = function(expr) {
return "not " + expr;
};
var binding = function(expr) {
return "(" + expr + ")";
}
var assetFilter = "is asset";
var trueFilter = true;
var falseFilter = false;
var bodyFilter = function(s) {
return "body ~= '" + s + "'";
}
var urlFilter = function(s) {
return "url ~= '" + s + "'";
}
}
start "filter expression"
= __ orExpr:OrExpr __ { return orExpr; }
ws "whitespace" = [ \t\n\r]
cc "control character" = [|&!()~"]
__ "optional whitespace" = ws*
OrExpr
= first:AndExpr __ "|" __ second:OrExpr
{ return or(first, second); }
/ AndExpr
AndExpr
= first:NotExpr __ "&" __ second:AndExpr
{ return and(first, second); }
/ first:NotExpr ws+ second:AndExpr
{ return and(first, second); }
/ NotExpr
NotExpr
= "!" __ expr:NotExpr
{ return not(expr); }
/ BindingExpr
BindingExpr
= "(" __ expr:OrExpr __ ")"
{ return binding(orExpr); }
/ Expr
Expr
= NullaryExpr
/ UnaryExpr
NullaryExpr
= BooleanLiteral
/ "~a" { return assetFilter; };
BooleanLiteral
= "true" { return trueFilter; }
/ "false" { return falseFilter; }
UnaryExpr
= "~b" ws+ s:StringLiteral { return bodyFilter(s); }
/ s:StringLiteral { return urlFilter(s); }
StringLiteral "string"
= '"' chars:DoubleStringChar* '"' { return chars.join(""); }
/ "'" chars:SingleStringChar* "'" { return chars.join(""); }
/ !cc chars:UnquotedStringChar+ { return chars.join(""); }
DoubleStringChar
= !["\\] char:. { return char; }
/ "\\" char:EscapeSequence { return char; }
SingleStringChar
= !['\\] char:. { return char; }
/ "\\" char:EscapeSequence { return char; }
UnquotedStringChar
= !ws char:. { return char; }
EscapeSequence
= ['"\\]
/ "n" { return "\n"; }
/ "r" { return "\r"; }
/ "t" { return "\t"; }

View File

164
web/src/js/store/store.js Normal file
View File

@ -0,0 +1,164 @@
function ListStore() {
EventEmitter.call(this);
this.reset();
}
_.extend(ListStore.prototype, EventEmitter.prototype, {
add: function (elem) {
if (elem.id in this._pos_map) {
return;
}
this._pos_map[elem.id] = this.list.length;
this.list.push(elem);
this.emit("add", elem);
},
update: function (elem) {
if (!(elem.id in this._pos_map)) {
return;
}
this.list[this._pos_map[elem.id]] = elem;
this.emit("update", elem);
},
remove: function (elem_id) {
if (!(elem.id in this._pos_map)) {
return;
}
this.list.splice(this._pos_map[elem_id], 1);
this._build_map();
this.emit("remove", elem_id);
},
reset: function (elems) {
this.list = elems || [];
this._build_map();
this.emit("recalculate", this.list);
},
_build_map: function () {
this._pos_map = {};
for (var i = 0; i < this.list.length; i++) {
var elem = this.list[i];
this._pos_map[elem.id] = i;
}
},
get: function (elem_id) {
return this.list[this._pos_map[elem_id]];
},
index: function (elem_id) {
return this._pos_map[elem_id];
}
});
function DictStore() {
EventEmitter.call(this);
this.reset();
}
_.extend(DictStore.prototype, EventEmitter.prototype, {
update: function (dict) {
_.merge(this.dict, dict);
this.emit("recalculate", this.dict);
},
reset: function (dict) {
this.dict = dict || {};
this.emit("recalculate", this.dict);
}
});
function LiveStoreMixin(type) {
this.type = type;
this._updates_before_fetch = undefined;
this._fetchxhr = false;
this.handle = this.handle.bind(this);
AppDispatcher.register(this.handle);
// Avoid double-fetch on startup.
if (!(window.ws && window.ws.readyState === WebSocket.CONNECTING)) {
this.fetch();
}
}
_.extend(LiveStoreMixin.prototype, {
handle: function (event) {
if (event.type === ActionTypes.CONNECTION_OPEN) {
return this.fetch();
}
if (event.type === this.type) {
if (event.cmd === StoreCmds.RESET) {
this.fetch();
} else if (this._updates_before_fetch) {
console.log("defer update", event);
this._updates_before_fetch.push(event);
} else {
this[event.cmd](event.data);
}
}
},
close: function () {
AppDispatcher.unregister(this.handle);
},
fetch: function (data) {
console.log("fetch " + this.type);
if (this._fetchxhr) {
this._fetchxhr.abort();
}
this._updates_before_fetch = []; // (JS: empty array is true)
if (data) {
this.handle_fetch(data);
} else {
this._fetchxhr = $.getJSON("/" + this.type)
.done(function (message) {
this.handle_fetch(message.data);
}.bind(this))
.fail(function () {
EventLogActions.add_event("Could not fetch " + this.type);
}.bind(this));
}
},
handle_fetch: function (data) {
this._fetchxhr = false;
console.log(this.type + " fetched.", this._updates_before_fetch);
this.reset(data);
var updates = this._updates_before_fetch;
this._updates_before_fetch = false;
for (var i = 0; i < updates.length; i++) {
this.handle(updates[i]);
}
},
});
function LiveListStore(type) {
ListStore.call(this);
LiveStoreMixin.call(this, type);
}
_.extend(LiveListStore.prototype, ListStore.prototype, LiveStoreMixin.prototype);
function LiveDictStore(type) {
DictStore.call(this);
LiveStoreMixin.call(this, type);
}
_.extend(LiveDictStore.prototype, DictStore.prototype, LiveStoreMixin.prototype);
function FlowStore() {
return new LiveListStore(ActionTypes.FLOW_STORE);
}
function SettingsStore() {
return new LiveDictStore(ActionTypes.SETTINGS_STORE);
}
function EventLogStore() {
LiveListStore.call(this, ActionTypes.EVENT_STORE);
}
_.extend(EventLogStore.prototype, LiveListStore.prototype, {
fetch: function(){
LiveListStore.prototype.fetch.apply(this, arguments);
// Make sure to display updates even if fetching all events failed.
// This way, we can send "fetch failed" log messages to the log.
if(this._fetchxhr){
this._fetchxhr.fail(function(){
this.handle_fetch(null);
}.bind(this));
}
}
});

99
web/src/js/store/view.js Normal file
View File

@ -0,0 +1,99 @@
function SortByStoreOrder(elem) {
return this.store.index(elem.id);
}
var default_sort = SortByStoreOrder;
var default_filt = function(elem){
return true;
};
function StoreView(store, filt, sortfun) {
EventEmitter.call(this);
filt = filt || default_filt;
sortfun = sortfun || default_sort;
this.store = store;
this.add = this.add.bind(this);
this.update = this.update.bind(this);
this.remove = this.remove.bind(this);
this.recalculate = this.recalculate.bind(this);
this.store.addListener("add", this.add);
this.store.addListener("update", this.update);
this.store.addListener("remove", this.remove);
this.store.addListener("recalculate", this.recalculate);
this.recalculate(this.store.list, filt, sortfun);
}
_.extend(StoreView.prototype, EventEmitter.prototype, {
close: function () {
this.store.removeListener("add", this.add);
this.store.removeListener("update", this.update);
this.store.removeListener("remove", this.remove);
this.store.removeListener("recalculate", this.recalculate);
},
recalculate: function (elems, filt, sortfun) {
if (filt) {
this.filt = filt;
}
if (sortfun) {
this.sortfun = sortfun.bind(this);
}
this.list = elems.filter(this.filt);
this.list.sort(function (a, b) {
return this.sortfun(a) - this.sortfun(b);
}.bind(this));
this.emit("recalculate");
},
index: function (elem) {
return _.sortedIndex(this.list, elem, this.sortfun);
},
add: function (elem) {
if (this.filt(elem)) {
var idx = this.index(elem);
if (idx === this.list.length) { //happens often, .push is way faster.
this.list.push(elem);
} else {
this.list.splice(idx, 0, elem);
}
this.emit("add", elem, idx);
}
},
update: function (elem) {
var idx;
var i = this.list.length;
// Search from the back, we usually update the latest entries.
while (i--) {
if (this.list[i].id === elem.id) {
idx = i;
break;
}
}
if (idx === -1) { //not contained in list
this.add(elem);
} else if (!this.filt(elem)) {
this.remove(elem.id);
} else {
if (this.sortfun(this.list[idx]) !== this.sortfun(elem)) { //sortpos has changed
this.remove(this.list[idx]);
this.add(elem);
} else {
this.list[idx] = elem;
this.emit("update", elem, idx);
}
}
},
remove: function (elem_id) {
var idx = this.list.length;
while (idx--) {
if (this.list[idx].id === elem_id) {
this.list.splice(idx, 1);
this.emit("remove", elem_id, idx);
break;
}
}
}
});

View File

@ -1,25 +0,0 @@
function EventEmitter() {
this.listeners = {};
}
EventEmitter.prototype.emit = function (event) {
if (!(event in this.listeners)) {
return;
}
var args = Array.prototype.slice.call(arguments, 1);
this.listeners[event].forEach(function (listener) {
listener.apply(this, args);
}.bind(this));
};
EventEmitter.prototype.addListener = function (event, f) {
this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push(f);
};
EventEmitter.prototype.removeListener = function (event, f) {
if (!(event in this.listeners)) {
return false;
}
var index = this.listeners[event].indexOf(f);
if (index >= 0) {
this.listeners[event].splice(index, 1);
}
};

View File

@ -1,99 +0,0 @@
//
// We have an EventLogView and an EventLogStore:
// The basic architecture is that one can request views on the event log
// from the store, which returns a view object and then deals with getting the data required for the view.
// The view object is accessed by React components and distributes updates etc.
//
// See also: components/EventLog.react.js
function EventLogView(store, live) {
EventEmitter.call(this);
this._store = store;
this.live = live;
this.log = [];
this.add = this.add.bind(this);
if (live) {
this._store.addListener(ActionTypes.ADD_EVENT, this.add);
}
}
_.extend(EventLogView.prototype, EventEmitter.prototype, {
close: function () {
this._store.removeListener(ActionTypes.ADD_EVENT, this.add);
},
getAll: function () {
return this.log;
},
add: function (entry) {
this.log.push(entry);
if(this.log.length > 200){
this.log.shift();
}
this.emit("change");
},
add_bulk: function (messages) {
var log = messages;
var last_id = log[log.length - 1].id;
var to_add = _.filter(this.log, function (entry) {
return entry.id > last_id;
});
this.log = log.concat(to_add);
this.emit("change");
}
});
function _EventLogStore() {
EventEmitter.call(this);
}
_.extend(_EventLogStore.prototype, EventEmitter.prototype, {
getView: function (since) {
var view = new EventLogView(this, !since);
return view;
/*
//TODO: Really do bulk retrieval of last messages.
window.setTimeout(function () {
view.add_bulk([
{
id: 1,
message: "Hello World"
},
{
id: 2,
message: "I was already transmitted as an event."
}
]);
}, 100);
var id = 2;
view.add({
id: id++,
message: "I was already transmitted as an event."
});
view.add({
id: id++,
message: "I was only transmitted as an event before the bulk was added.."
});
window.setInterval(function () {
view.add({
id: id++,
message: "."
});
}, 1000);
return view;
*/
},
handle: function (action) {
switch (action.type) {
case ActionTypes.ADD_EVENT:
this.emit(ActionTypes.ADD_EVENT, action.data);
break;
default:
return;
}
}
});
var EventLogStore = new _EventLogStore();
AppDispatcher.register(EventLogStore.handle.bind(EventLogStore));

View File

@ -1,91 +0,0 @@
function FlowView(store, live) {
EventEmitter.call(this);
this._store = store;
this.live = live;
this.flows = [];
this.add = this.add.bind(this);
this.update = this.update.bind(this);
if (live) {
this._store.addListener(ActionTypes.ADD_FLOW, this.add);
this._store.addListener(ActionTypes.UPDATE_FLOW, this.update);
}
}
_.extend(FlowView.prototype, EventEmitter.prototype, {
close: function () {
this._store.removeListener(ActionTypes.ADD_FLOW, this.add);
this._store.removeListener(ActionTypes.UPDATE_FLOW, this.update);
},
getAll: function () {
return this.flows;
},
add: function (flow) {
return this.update(flow);
},
add_bulk: function (flows) {
//Treat all previously received updates as newer than the bulk update.
//If they weren't newer, we're about to receive an update for them very soon.
var updates = this.flows;
this.flows = flows;
updates.forEach(function(flow){
this._update(flow);
}.bind(this));
this.emit("change");
},
_update: function(flow){
var idx = _.findIndex(this.flows, function(f){
return flow.id === f.id;
});
if(idx < 0){
this.flows.push(flow);
//if(this.flows.length > 100){
// this.flows.shift();
//}
} else {
this.flows[idx] = flow;
}
},
update: function(flow){
this._update(flow);
this.emit("change");
},
});
function _FlowStore() {
EventEmitter.call(this);
}
_.extend(_FlowStore.prototype, EventEmitter.prototype, {
getView: function (since) {
var view = new FlowView(this, !since);
$.getJSON("/static/flows.json", function(flows){
flows = flows.concat(_.cloneDeep(flows)).concat(_.cloneDeep(flows));
var id = 1;
flows.forEach(function(flow){
flow.id = "uuid-" + id++;
});
view.add_bulk(flows);
});
return view;
},
handle: function (action) {
switch (action.type) {
case ActionTypes.ADD_FLOW:
case ActionTypes.UPDATE_FLOW:
this.emit(action.type, action.data);
break;
default:
return;
}
}
});
var FlowStore = new _FlowStore();
AppDispatcher.register(FlowStore.handle.bind(FlowStore));

View File

@ -1,28 +0,0 @@
function _SettingsStore() {
EventEmitter.call(this);
//FIXME: What do we do if we haven't requested anything from the server yet?
this.settings = {
version: "0.12",
showEventLog: true,
mode: "transparent",
};
}
_.extend(_SettingsStore.prototype, EventEmitter.prototype, {
getAll: function () {
return this.settings;
},
handle: function (action) {
switch (action.type) {
case ActionTypes.UPDATE_SETTINGS:
this.settings = action.settings;
this.emit("change");
break;
default:
return;
}
}
});
var SettingsStore = new _SettingsStore();
AppDispatcher.register(SettingsStore.handle.bind(SettingsStore));

View File

@ -12,6 +12,7 @@ var AutoScrollMixin = {
},
};
var StickyHeadMixin = {
adjustHead: function () {
// Abusing CSS transforms to set the element
@ -21,6 +22,7 @@ var StickyHeadMixin = {
}
};
var Key = {
UP: 38,
DOWN: 40,
@ -38,17 +40,19 @@ var Key = {
L: 76
};
var formatSize = function (bytes) {
var size = bytes;
var prefix = ["B", "KB", "MB", "GB", "TB"];
var i=0;
while (Math.abs(size) >= 1024 && i < prefix.length-1) {
var i = 0;
while (Math.abs(size) >= 1024 && i < prefix.length - 1) {
i++;
size = size / 1024;
}
return (Math.floor(size * 100) / 100.0).toFixed(2) + prefix[i];
};
var formatTimeDelta = function (milliseconds) {
var time = milliseconds;
var prefix = ["ms", "s", "min", "h"];
@ -60,3 +64,40 @@ var formatTimeDelta = function (milliseconds) {
}
return Math.round(time) + prefix[i];
};
var formatTimeStamp = function (seconds) {
var ts = (new Date(seconds * 1000)).toISOString();
return ts.replace("T", " ").replace("Z", "");
};
function EventEmitter() {
this.listeners = {};
}
EventEmitter.prototype.emit = function (event) {
if (!(event in this.listeners)) {
return;
}
var args = Array.prototype.slice.call(arguments, 1);
this.listeners[event].forEach(function (listener) {
listener.apply(this, args);
}.bind(this));
};
EventEmitter.prototype.addListener = function (events, f) {
events.split(" ").forEach(function (event) {
this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push(f);
}.bind(this));
};
EventEmitter.prototype.removeListener = function (events, f) {
if (!(events in this.listeners)) {
return false;
}
events.split(" ").forEach(function (event) {
var index = this.listeners[event].indexOf(f);
if (index >= 0) {
this.listeners[event].splice(index, 1);
}
}.bind(this));
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,12 @@
/*!
* QUnit 1.15.0
* QUnit 1.16.0
* http://qunitjs.com/
*
* Copyright 2014 jQuery Foundation and other contributors
* Copyright 2006, 2014 jQuery Foundation and other contributors
* Released under the MIT license
* http://jquery.org/license
*
* Date: 2014-08-08T16:00Z
* Date: 2014-12-03T16:32Z
*/
/** Font Family and Sizes */
@ -91,6 +91,14 @@
list-style-position: inside;
}
#qunit-tests > li {
display: none;
}
#qunit-tests li.pass, #qunit-tests li.running, #qunit-tests li.fail {
display: list-item;
}
#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
display: none;
}
@ -99,6 +107,10 @@
cursor: pointer;
}
#qunit-tests li.skipped strong {
cursor: default;
}
#qunit-tests li a {
padding: 0.5em;
color: #C2CCD1;
@ -211,6 +223,21 @@
#qunit-banner.qunit-fail { background-color: #EE5757; }
/*** Skipped tests */
#qunit-tests .skipped {
background-color: #EBECE9;
}
#qunit-tests .qunit-skipped-label {
background-color: #F4FF77;
display: inline-block;
font-style: normal;
color: #366097;
line-height: 1.8em;
padding: 0 0.5em;
margin: -0.4em 0.4em -0.4em 0;
}
/** Result */

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff