mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-26 18:18:25 +00:00
commit
01fa5d3f07
@ -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
|
||||
|
@ -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()
|
||||
|
@ -117,7 +117,10 @@ class HTTPMessage(stateobject.StateObject):
|
||||
def get_state(self, short=False):
|
||||
ret = super(HTTPMessage, self).get_state(short)
|
||||
if short:
|
||||
if self.content:
|
||||
ret["contentLength"] = len(self.content)
|
||||
else:
|
||||
ret["contentLength"] = 0
|
||||
return ret
|
||||
|
||||
def get_decoded_content(self):
|
||||
|
@ -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)
|
@ -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)
|
||||
|
@ -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
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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\"'")
|
||||
|
||||
|
@ -10,8 +10,7 @@
|
||||
"qunit": "",
|
||||
"benchmark": "",
|
||||
"benchmarkjs-runner": "",
|
||||
"bootstrap": "",
|
||||
"react-bootstrap": ""
|
||||
"bootstrap": ""
|
||||
},
|
||||
"install": {
|
||||
"path": "src/vendor",
|
||||
|
@ -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',
|
||||
|
@ -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": ""
|
||||
}
|
||||
|
2012
web/src/flows.json
2012
web/src/flows.json
File diff suppressed because it is too large
Load Diff
@ -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) {
|
||||
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++
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -1,5 +1,3 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var LogMessage = React.createClass({
|
||||
render: function () {
|
||||
var entry = this.props.entry;
|
||||
@ -26,38 +24,70 @@ var LogMessage = React.createClass({
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
var messages = this.state.log.map(function(row) {
|
||||
if(!this.props.filter[row.level]){
|
||||
return null;
|
||||
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);
|
||||
}
|
||||
return <LogMessage key={row.id} entry={row}/>;
|
||||
}.bind(this));
|
||||
return <pre>{messages}</pre>;
|
||||
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 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 () {
|
||||
@ -94,10 +124,9 @@ var EventLog = React.createClass({
|
||||
});
|
||||
},
|
||||
toggleLevel: function (level) {
|
||||
var filter = this.state.filter;
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
@ -1,14 +1,12 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var FlowDetailNav = React.createClass({
|
||||
render: function () {
|
||||
|
||||
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="#"
|
||||
@ -49,7 +47,7 @@ var FlowDetailRequest = React.createClass({
|
||||
var first_line = [
|
||||
flow.request.method,
|
||||
RequestUtils.pretty_url(flow.request),
|
||||
"HTTP/"+ flow.response.httpversion.join(".")
|
||||
"HTTP/" + flow.request.httpversion.join(".")
|
||||
].join(" ");
|
||||
var content = null;
|
||||
if (flow.request.contentLength > 0) {
|
||||
@ -99,6 +97,20 @@ var FlowDetailResponse = React.createClass({
|
||||
}
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var TimeStamp = React.createClass({
|
||||
render: function () {
|
||||
|
||||
@ -107,8 +119,7 @@ var TimeStamp = React.createClass({
|
||||
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) {
|
||||
@ -118,7 +129,10 @@ var TimeStamp = React.createClass({
|
||||
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>;
|
||||
}
|
||||
});
|
||||
|
||||
@ -130,12 +144,20 @@ var ConnectionInfo = React.createClass({
|
||||
|
||||
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>;
|
||||
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>
|
||||
@ -225,7 +247,7 @@ var Timing = React.createClass({
|
||||
timestamps = _.sortBy(timestamps, 't');
|
||||
|
||||
var rows = timestamps.map(function (e) {
|
||||
return TimeStamp(e);
|
||||
return <TimeStamp {...e}/>;
|
||||
});
|
||||
|
||||
return (
|
||||
@ -264,35 +286,65 @@ var FlowDetailConnectionInfo = React.createClass({
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
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]);
|
||||
},
|
||||
selectTab: function (panel) {
|
||||
this.replaceWith(
|
||||
"flow",
|
||||
{
|
||||
flowId: this.getParams().flowId,
|
||||
detailTab: panel
|
||||
}
|
||||
);
|
||||
},
|
||||
render: function () {
|
||||
var flow = JSON.stringify(this.props.flow, null, 2);
|
||||
var Tab = tabs[this.props.active];
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,3 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
|
||||
var TLSColumn = React.createClass({
|
||||
statics: {
|
||||
renderTitle: function () {
|
||||
@ -55,7 +52,9 @@ var IconColumn = React.createClass({
|
||||
|
||||
|
||||
icon += " resource-icon";
|
||||
return <td className="col-icon"><div className={icon}></div></td>;
|
||||
return <td className="col-icon">
|
||||
<div className={icon}></div>
|
||||
</td>;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,10 +1,8 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var FlowRow = React.createClass({
|
||||
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) {
|
||||
@ -16,11 +14,13 @@ var FlowRow = React.createClass({
|
||||
</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;
|
||||
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)
|
||||
//);
|
||||
}
|
||||
});
|
||||
|
||||
@ -29,64 +29,79 @@ var FlowTableHead = React.createClass({
|
||||
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}/>
|
||||
<tbody ref="body">
|
||||
{ this.getPlaceholderTop(flows.length) }
|
||||
{rows}
|
||||
{ this.getPlaceholderBottom(flows.length) }
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,5 +1,3 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var Footer = React.createClass({
|
||||
render: function () {
|
||||
var mode = this.props.settings.mode;
|
||||
|
@ -1,5 +1,3 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var MainMenu = React.createClass({
|
||||
statics: {
|
||||
title: "Traffic",
|
||||
@ -10,11 +8,20 @@ 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>
|
||||
Display Event Log
|
||||
</button>
|
||||
|
||||
<button className="btn btn-default" onClick={this.clearFlows}>
|
||||
<i className="fa fa-eraser"></i>
|
||||
Clear Flows
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@ -43,23 +50,95 @@ 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) {
|
||||
@ -83,7 +162,7 @@ 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">
|
||||
|
@ -1,62 +1,83 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var MainView = React.createClass({
|
||||
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();
|
||||
},
|
||||
onFlowChange: function () {
|
||||
this.setState({
|
||||
flows: this.flowStore.getAll()
|
||||
});
|
||||
},
|
||||
selectDetailTab: function(panel) {
|
||||
ReactRouter.replaceWith(
|
||||
"flow",
|
||||
{
|
||||
flowId: this.props.params.flowId,
|
||||
detailTab: panel
|
||||
}
|
||||
);
|
||||
this.closeView();
|
||||
},
|
||||
selectFlow: function (flow) {
|
||||
if (flow) {
|
||||
ReactRouter.replaceWith(
|
||||
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;
|
||||
}
|
||||
this.selectFlow(this.state.flows[index]);
|
||||
}
|
||||
index = Math.min(
|
||||
Math.max(0, index + shift),
|
||||
flows.length - 1);
|
||||
}
|
||||
this.selectFlow(flows[index]);
|
||||
},
|
||||
onKeyDown: function (e) {
|
||||
switch (e.keyCode) {
|
||||
@ -95,19 +116,20 @@ var MainView = React.createClass({
|
||||
console.debug("keydown", e.keyCode);
|
||||
return;
|
||||
}
|
||||
return false;
|
||||
e.preventDefault();
|
||||
},
|
||||
getSelected: function(){
|
||||
return this.props.flowStore.get(this.getParams().flowId);
|
||||
},
|
||||
render: function () {
|
||||
var selected = _.find(this.state.flows, { id: this.props.params.flowId });
|
||||
var selected = this.getSelected();
|
||||
|
||||
var details;
|
||||
if (selected) {
|
||||
details = (
|
||||
<FlowDetail ref="flowDetails"
|
||||
flow={selected}
|
||||
selectTab={this.selectDetailTab}
|
||||
active={this.props.params.detailTab}/>
|
||||
);
|
||||
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}
|
||||
view={this.state.view}
|
||||
selectFlow={this.selectFlow}
|
||||
selected={selected} />
|
||||
{ details ? <Splitter/> : null }
|
||||
{details}
|
||||
</div>
|
||||
);
|
||||
|
@ -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()});
|
||||
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">
|
||||
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>
|
||||
</Routes>
|
||||
);
|
@ -1,5 +1,3 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
//React utils. For other utilities, see ../utils.js
|
||||
|
||||
var Splitter = React.createClass({
|
||||
@ -53,6 +51,7 @@ var Splitter = React.createClass({
|
||||
this.setState({
|
||||
applied: true
|
||||
});
|
||||
this.onResize();
|
||||
},
|
||||
onMouseMove: function (e) {
|
||||
var dX = 0, dY = 0;
|
||||
@ -63,6 +62,13 @@ var Splitter = React.createClass({
|
||||
}
|
||||
this.getDOMNode().style.transform = "translate(" + dX + "px," + dY + "px)";
|
||||
},
|
||||
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;
|
||||
@ -79,7 +85,7 @@ var Splitter = React.createClass({
|
||||
applied: false
|
||||
});
|
||||
}
|
||||
|
||||
this.onResize();
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
this.reset(true);
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
81
web/src/js/components/virtualscroll.jsx.js
Normal file
81
web/src/js/components/virtualscroll.jsx.js
Normal 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;
|
||||
}
|
||||
},
|
||||
};
|
@ -1,33 +1,24 @@
|
||||
function _Connection(url) {
|
||||
this.url = url;
|
||||
}
|
||||
_Connection.prototype.init = function () {
|
||||
this.openWebSocketConnection();
|
||||
};
|
||||
_Connection.prototype.openWebSocketConnection = function () {
|
||||
this.ws = new WebSocket(this.url.replace("http", "ws"));
|
||||
var ws = this.ws;
|
||||
function Connection(url) {
|
||||
|
||||
ws.onopen = this.onopen.bind(this);
|
||||
ws.onmessage = this.onmessage.bind(this);
|
||||
ws.onerror = this.onerror.bind(this);
|
||||
ws.onclose = this.onclose.bind(this);
|
||||
if (url[0] === "/") {
|
||||
url = location.origin.replace("http", "ws") + url;
|
||||
}
|
||||
|
||||
var ws = new WebSocket(url);
|
||||
ws.onopen = function () {
|
||||
ConnectionActions.open();
|
||||
};
|
||||
_Connection.prototype.onopen = function (open) {
|
||||
console.debug("onopen", this, arguments);
|
||||
};
|
||||
_Connection.prototype.onmessage = function (message) {
|
||||
//AppDispatcher.dispatchServerAction(...);
|
||||
ws.onmessage = function (message) {
|
||||
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);
|
||||
ws.onerror = function () {
|
||||
ConnectionActions.error();
|
||||
EventLogActions.add_event("WebSocket connection error.");
|
||||
};
|
||||
_Connection.prototype.onclose = function (close) {
|
||||
EventLogActions.add_event("WebSocket Connection closed.");
|
||||
console.debug("onclose", this, arguments);
|
||||
ws.onclose = function () {
|
||||
ConnectionActions.close();
|
||||
EventLogActions.add_event("WebSocket connection closed.");
|
||||
};
|
||||
|
||||
var Connection = new _Connection(location.origin + "/updates");
|
||||
return ws;
|
||||
}
|
@ -11,9 +11,9 @@ 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) {
|
||||
|
93
web/src/js/filt/filt.pegjs
Normal file
93
web/src/js/filt/filt.pegjs
Normal 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"; }
|
0
web/src/js/store/settingstore.js
Normal file
0
web/src/js/store/settingstore.js
Normal file
164
web/src/js/store/store.js
Normal file
164
web/src/js/store/store.js
Normal 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
99
web/src/js/store/view.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
@ -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);
|
||||
}
|
||||
};
|
@ -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));
|
@ -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));
|
@ -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));
|
@ -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,6 +40,7 @@ var Key = {
|
||||
L: 76
|
||||
};
|
||||
|
||||
|
||||
var formatSize = function (bytes) {
|
||||
var size = bytes;
|
||||
var prefix = ["B", "KB", "MB", "GB", "TB"];
|
||||
@ -49,6 +52,7 @@ var formatSize = function (bytes) {
|
||||
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));
|
||||
};
|
503
web/src/vendor/bootstrap/bootstrap.css
vendored
503
web/src/vendor/bootstrap/bootstrap.css
vendored
File diff suppressed because it is too large
Load Diff
508
web/src/vendor/bootstrap/bootstrap.js
vendored
508
web/src/vendor/bootstrap/bootstrap.js
vendored
File diff suppressed because it is too large
Load Diff
33
web/src/vendor/qunit/qunit.css
vendored
33
web/src/vendor/qunit/qunit.css
vendored
@ -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 */
|
||||
|
||||
|
1340
web/src/vendor/qunit/qunit.js
vendored
1340
web/src/vendor/qunit/qunit.js
vendored
File diff suppressed because it is too large
Load Diff
5346
web/src/vendor/react-bootstrap/react-bootstrap.js
vendored
5346
web/src/vendor/react-bootstrap/react-bootstrap.js
vendored
File diff suppressed because it is too large
Load Diff
4099
web/src/vendor/react-router/react-router.js
vendored
4099
web/src/vendor/react-router/react-router.js
vendored
File diff suppressed because it is too large
Load Diff
4629
web/src/vendor/react/JSXTransformer.js
vendored
4629
web/src/vendor/react/JSXTransformer.js
vendored
File diff suppressed because one or more lines are too long
6292
web/src/vendor/react/react-with-addons.js
vendored
6292
web/src/vendor/react/react-with-addons.js
vendored
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user