aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMaximilian Hils <git@maximilianhils.com>2014-09-18 02:22:10 +0200
committerMaximilian Hils <git@maximilianhils.com>2014-09-18 02:22:10 +0200
commit0d64cc93278d39bd4c87cf5110d326f57574c8a1 (patch)
tree41cdb8ea3fdd51304c23234ca8fd7c2372d96988
parent6a161be6b4c526fcc5f6581c7faff00a2c976f37 (diff)
downloadmitmproxy-0d64cc93278d39bd4c87cf5110d326f57574c8a1.tar.gz
mitmproxy-0d64cc93278d39bd4c87cf5110d326f57574c8a1.tar.bz2
mitmproxy-0d64cc93278d39bd4c87cf5110d326f57574c8a1.zip
flowtable: add selection indicator, add keyboard navigation
-rw-r--r--libmproxy/protocol/primitives.py3
-rw-r--r--libmproxy/web/static/css/app.css7
-rw-r--r--libmproxy/web/static/flows.json26
-rw-r--r--libmproxy/web/static/js/app.js178
-rw-r--r--web/gulpfile.js1
-rw-r--r--web/src/css/flowtable.less5
-rw-r--r--web/src/css/layout.less2
-rw-r--r--web/src/js/components/flowtable-columns.jsx.js95
-rw-r--r--web/src/js/components/flowtable.jsx.js192
-rw-r--r--web/src/js/components/proxyapp.jsx.js2
-rw-r--r--web/src/js/stores/flowstore.js9
-rw-r--r--web/src/js/utils.js10
12 files changed, 376 insertions, 154 deletions
diff --git a/libmproxy/protocol/primitives.py b/libmproxy/protocol/primitives.py
index 3d87e888..519693db 100644
--- a/libmproxy/protocol/primitives.py
+++ b/libmproxy/protocol/primitives.py
@@ -1,5 +1,6 @@
from __future__ import absolute_import
import copy
+import uuid
import netlib.tcp
from .. import stateobject, utils, version
from ..proxy.connection import ClientConnection, ServerConnection
@@ -60,6 +61,7 @@ class Flow(stateobject.StateObject):
"""
def __init__(self, conntype, client_conn, server_conn, live=None):
self.conntype = conntype
+ self.id = str(uuid.uuid4())
self.client_conn = client_conn
"""@type: ClientConnection"""
self.server_conn = server_conn
@@ -72,6 +74,7 @@ class Flow(stateobject.StateObject):
self._backup = None
_stateobject_attributes = dict(
+ id=str,
error=Error,
client_conn=ClientConnection,
server_conn=ServerConnection,
diff --git a/libmproxy/web/static/css/app.css b/libmproxy/web/static/css/app.css
index dcc31d18..673beb3f 100644
--- a/libmproxy/web/static/css/app.css
+++ b/libmproxy/web/static/css/app.css
@@ -56,7 +56,7 @@ body,
#container > .eventlog {
flex: 0 0 auto;
}
-#main {
+main {
flex: 1 1 auto;
overflow: auto;
}
@@ -111,6 +111,9 @@ header .menu {
.flow-table tr {
cursor: pointer;
}
+.flow-table tr.selected {
+ background-color: rgba(193, 215, 235, 0.5) !important;
+}
.flow-table td {
overflow: hidden;
white-space: nowrap;
@@ -119,7 +122,7 @@ header .menu {
.flow-table tr:nth-child(even) {
background-color: rgba(0, 0, 0, 0.05);
}
-.flow-table .col-tls {
+.flow-table .col-tls {
width: 10px;
}
.flow-table .col-tls-https {
diff --git a/libmproxy/web/static/flows.json b/libmproxy/web/static/flows.json
index bdbfd5cc..ece178a7 100644
--- a/libmproxy/web/static/flows.json
+++ b/libmproxy/web/static/flows.json
@@ -1,4 +1,5 @@
[{
+ "id": "b5e5483c-e124-45bb-aa2e-360706e03ef4",
"request": {
"timestamp_end": 1410651311.107,
"timestamp_start": 1410651311.106,
@@ -157,6 +158,7 @@
}
},
{
+ "id": "85e9781f-d81d-43ca-a694-2cd86c76d991",
"request": {
"timestamp_end": 1410651311.657,
"timestamp_start": 1410651311.653,
@@ -319,6 +321,7 @@
}
},
{
+ "id": "1bf281fd-e02a-423c-a69c-aa65657bc3dd",
"request": {
"timestamp_end": 1410651312.362,
"timestamp_start": 1410651312.359,
@@ -485,6 +488,7 @@
}
},
{
+ "id": "833253a0-f7dd-48c7-893c-1f13a38a71ce",
"request": {
"timestamp_end": 1410651312.389,
"timestamp_start": 1410651312.368,
@@ -651,6 +655,7 @@
}
},
{
+ "id": "152d8e71-2469-4034-8d6d-11099bbb4248",
"request": {
"timestamp_end": 1410651312.386,
"timestamp_start": 1410651312.368,
@@ -817,6 +822,7 @@
}
},
{
+ "id": "b3758e4d-7bae-4771-b154-e100c0722d00",
"request": {
"timestamp_end": 1410651373.965,
"timestamp_start": 1410651373.963,
@@ -947,6 +953,7 @@
}
},
{
+ "id": "ea9e47ab-fd7b-4463-bfea-cfd64cc5f78d",
"request": {
"timestamp_end": 1410651374.391,
"timestamp_start": 1410651374.387,
@@ -1081,6 +1088,7 @@
}
},
{
+ "id": "13ee4cd1-08e0-43ef-9bee-56fc0d9cbf3f",
"request": {
"timestamp_end": 1410651374.396,
"timestamp_start": 1410651374.394,
@@ -1211,7 +1219,8 @@
}
},
{
- "request": {
+ "id": "5c50e1fc-5ac4-4748-aed1-c969ede63e4e",
+ "request": {
"timestamp_end": 1410651374.795,
"timestamp_start": 1410651374.793,
"form_in": "absolute",
@@ -1361,7 +1370,8 @@
}
},
{
- "request": {
+ "id": "0285a0b2-380e-43eb-a7a9-a18893950216",
+ "request": {
"timestamp_end": 1410651375.084,
"timestamp_start": 1410651375.078,
"form_in": "absolute",
@@ -1507,7 +1517,8 @@
}
},
{
- "request": {
+ "id": "c9af9c71-dc68-462e-8446-f3a4b2782400",
+ "request": {
"timestamp_end": 1410651374.778,
"timestamp_start": 1410651374.766,
"form_in": "absolute",
@@ -1637,7 +1648,8 @@
}
},
{
- "request": {
+ "id": "310386ab-3ae1-4129-9a2e-8dd2ce60ecdb",
+ "request": {
"timestamp_end": 1410651374.778,
"timestamp_start": 1410651374.766,
"form_in": "absolute",
@@ -1767,7 +1779,8 @@
}
},
{
- "request": {
+ "id": "b92e5f6e-bb0f-4e47-a50c-ef4072ea40b3",
+ "request": {
"timestamp_end": 1410651376.078,
"timestamp_start": 1410651376.075,
"form_in": "absolute",
@@ -1889,7 +1902,8 @@
}
},
{
- "request": {
+ "id": "597d086f-d836-49e3-85bb-77a983bed87f",
+ "request": {
"timestamp_end": 1410651376.282,
"timestamp_start": 1410651376.279,
"form_in": "absolute",
diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js
index df1c91de..9c184e50 100644
--- a/libmproxy/web/static/js/app.js
+++ b/libmproxy/web/static/js/app.js
@@ -12,6 +12,15 @@ var AutoScrollMixin = {
},
};
+
+var Key = {
+ UP: 38,
+ DOWN: 40,
+ LEFT: 37,
+ RIGHT: 39,
+ ENTER: 13,
+ ESC: 27
+}
const PayloadSources = {
VIEW: "view",
SERVER: "server"
@@ -274,9 +283,8 @@ _.extend(FlowView.prototype, EventEmitter.prototype, {
this.emit("change");
},
_update: function(flow){
- console.debug("FIXME: Use UUID");
var idx = _.findIndex(this.flows, function(f){
- return flow.request.timestamp_start == f.request.timestamp_start;
+ return flow.id === f.id;
});
if(idx < 0){
@@ -300,7 +308,13 @@ _.extend(_FlowStore.prototype, EventEmitter.prototype, {
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;
@@ -447,38 +461,6 @@ var Header = React.createClass({displayName: 'Header',
/** @jsx React.DOM */
-var FlowRow = React.createClass({displayName: 'FlowRow',
- render: function(){
- var flow = this.props.flow;
- var columns = this.props.columns.map(function(column){
- return column({
- key: column.displayName,
- flow: flow
- });
- }.bind(this));
- return React.DOM.tr({onClick: this.props.onClick}, columns);
- }
-});
-
-var FlowTableHead = React.createClass({displayName: 'FlowTableHead',
- render: function(){
- var columns = this.props.columns.map(function(column){
- return column.renderTitle();
- }.bind(this));
- return React.DOM.thead(null, columns);
- }
-});
-
-var FlowTableBody = React.createClass({displayName: 'FlowTableBody',
- render: function(){
- var rows = this.props.flows.map(function(flow){
- //TODO: Add UUID
- return FlowRow({onClick: this.props.onClick, flow: flow, columns: this.props.columns});
- }.bind(this));
- return React.DOM.tbody(null, rows);
- }
-});
-
var TLSColumn = React.createClass({displayName: 'TLSColumn',
statics: {
@@ -573,6 +555,54 @@ var TimeColumn = React.createClass({displayName: 'TimeColumn',
var all_columns = [TLSColumn, IconColumn, PathColumn, MethodColumn, StatusColumn, TimeColumn];
+/** @jsx React.DOM */
+
+var FlowRow = React.createClass({displayName: 'FlowRow',
+ render: function(){
+ var flow = this.props.flow;
+ var columns = this.props.columns.map(function(column){
+ return column({
+ key: column.displayName,
+ flow: flow
+ });
+ }.bind(this));
+ var className = "";
+ if(this.props.selected){
+ className += "selected";
+ }
+ return (
+ React.DOM.tr({className: className, onClick: this.props.selectFlow.bind(null, flow)},
+ columns
+ ));
+ }
+});
+
+var FlowTableHead = React.createClass({displayName: 'FlowTableHead',
+ render: function(){
+ var columns = this.props.columns.map(function(column){
+ return column.renderTitle();
+ }.bind(this));
+ return React.DOM.thead(null, columns);
+ }
+});
+
+var FlowTableBody = React.createClass({displayName: 'FlowTableBody',
+ 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 React.DOM.tbody({onKeyDown: this.props.onKeyDown, tabIndex: "0"}, rows);
+ }
+});
+
+
var FlowTable = React.createClass({displayName: 'FlowTable',
getInitialState: function () {
return {
@@ -593,18 +623,88 @@ var FlowTable = React.createClass({displayName: 'FlowTable',
flows: this.flowStore.getAll()
});
},
- onClick: function(e){
- console.log("rowclick", e);
+ selectFlow: function(flow){
+ this.setState({
+ selected: 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;
+ }
+ },
+ selectRowRelative: function(i){
+ var index;
+ if(!this.state.selected){
+ if(i > 0){
+ index = this.flows.length-1;
+ } else {
+ index = 0;
+ }
+ } else {
+ index = _.findIndex(this.state.flows, function(f){
+ return f === this.state.selected;
+ }.bind(this));
+ index = Math.min(Math.max(0, index+i), this.state.flows.length-1);
+ }
+ this.selectFlow(this.state.flows[index]);
+ },
+ onKeyDown: function(e){
+ switch(e.keyCode){
+ case Key.DOWN:
+ this.selectRowRelative(+1);
+ return false;
+ break;
+ case Key.UP:
+ this.selectRowRelative(-1);
+ return false;
+ break;
+ case Key.ENTER:
+ console.log("Open details pane...", this.state.selected);
+ break;
+ case Key.ESC:
+ console.log("")
+ default:
+ console.debug("keydown", e.keyCode);
+ return;
+ }
+ return false;
+ },
+ onScroll: function(e){
+ //Abusing CSS transforms to set thead into position:fixed.
+ var head = this.refs.head.getDOMNode();
+ head.style.transform = "translate(0,"+this.getDOMNode().scrollTop+"px)";
},
render: function () {
var flows = this.state.flows.map(function(flow){
return React.DOM.div(null, flow.request.method, " ", flow.request.scheme, "://", flow.request.host, flow.request.path);
});
return (
+ React.DOM.main({onScroll: this.onScroll},
React.DOM.table({className: "flow-table"},
- FlowTableHead({columns: this.state.columns}),
- FlowTableBody({onClick: this.onClick, columns: this.state.columns, flows: this.state.flows})
+ FlowTableHead({ref: "head",
+ columns: this.state.columns}),
+ FlowTableBody({ref: "body",
+ selectFlow: this.selectFlow,
+ onKeyDown: this.onKeyDown,
+ selected: this.state.selected,
+ columns: this.state.columns,
+ flows: this.state.flows})
)
+ )
);
}
});
@@ -691,7 +791,7 @@ var ProxyAppMain = React.createClass({displayName: 'ProxyAppMain',
return (
React.DOM.div({id: "container"},
Header({settings: this.state.settings}),
- React.DOM.div({id: "main"}, this.props.activeRouteHandler(null)),
+ this.props.activeRouteHandler(null),
this.state.settings.showEventLog ? EventLog(null) : null,
Footer({settings: this.state.settings})
)
diff --git a/web/gulpfile.js b/web/gulpfile.js
index 6b1758d5..a0f7825b 100644
--- a/web/gulpfile.js
+++ b/web/gulpfile.js
@@ -42,6 +42,7 @@ var path = {
'js/stores/flowstore.js',
'js/connection.js',
'js/components/header.jsx.js',
+ 'js/components/flowtable-columns.jsx.js',
'js/components/flowtable.jsx.js',
'js/components/eventlog.jsx.js',
'js/components/footer.jsx.js',
diff --git a/web/src/css/flowtable.less b/web/src/css/flowtable.less
index f96b7abf..d78fe31c 100644
--- a/web/src/css/flowtable.less
+++ b/web/src/css/flowtable.less
@@ -8,6 +8,9 @@
tr {
cursor: pointer;
+ &.selected {
+ background-color: hsla(209, 52%, 84%, 0.5) !important;
+ }
}
td {
@@ -19,8 +22,6 @@
//tr:nth-child(odd) { background-color : white; }
tr:nth-child(even) { background-color : rgba(0,0,0,0.05); }
//tr:hover { background-color : hsla(209, 52%, 84%, 0.5); }
-​
-
.col-tls {
width: 10px;
diff --git a/web/src/css/layout.less b/web/src/css/layout.less
index c8fad204..320baee8 100644
--- a/web/src/css/layout.less
+++ b/web/src/css/layout.less
@@ -13,7 +13,7 @@ html, body, #container {
}
}
-#main {
+main {
flex: 1 1 auto;
overflow: auto;
} \ No newline at end of file
diff --git a/web/src/js/components/flowtable-columns.jsx.js b/web/src/js/components/flowtable-columns.jsx.js
new file mode 100644
index 00000000..e0cee365
--- /dev/null
+++ b/web/src/js/components/flowtable-columns.jsx.js
@@ -0,0 +1,95 @@
+/** @jsx React.DOM */
+
+
+var TLSColumn = React.createClass({
+ statics: {
+ renderTitle: function(){
+ return <th key="tls" className="col-tls"></th>;
+ }
+ },
+ render: function(){
+ var flow = this.props.flow;
+ var ssl = (flow.request.scheme == "https");
+ return <td className={ssl ? "col-tls-https" : "col-tls-http"}></td>;
+ }
+});
+
+
+var IconColumn = React.createClass({
+ statics: {
+ renderTitle: function(){
+ return <th key="icon" className="col-icon"></th>;
+ }
+ },
+ render: function(){
+ var flow = this.props.flow;
+ return <td className="resource-icon resource-icon-plain"></td>;
+ }
+});
+
+var PathColumn = React.createClass({
+ statics: {
+ renderTitle: function(){
+ return <th key="path" className="col-path">Path</th>;
+ }
+ },
+ render: function(){
+ var flow = this.props.flow;
+ return <td>{flow.request.scheme + "://" + flow.request.host + flow.request.path}</td>;
+ }
+});
+
+
+var MethodColumn = React.createClass({
+ statics: {
+ renderTitle: function(){
+ return <th key="method" className="col-method">Method</th>;
+ }
+ },
+ render: function(){
+ var flow = this.props.flow;
+ return <td>{flow.request.method}</td>;
+ }
+});
+
+
+var StatusColumn = React.createClass({
+ statics: {
+ renderTitle: function(){
+ return <th key="status" className="col-status">Status</th>;
+ }
+ },
+ render: function(){
+ var flow = this.props.flow;
+ var status;
+ if(flow.response){
+ status = flow.response.code;
+ } else {
+ status = null;
+ }
+ return <td>{status}</td>;
+ }
+});
+
+
+var TimeColumn = React.createClass({
+ statics: {
+ renderTitle: function(){
+ return <th key="time" className="col-time">Time</th>;
+ }
+ },
+ render: function(){
+ var flow = this.props.flow;
+ var time;
+ if(flow.response){
+ time = Math.round(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))+"ms";
+ } else {
+ time = "...";
+ }
+ return <td>{time}</td>;
+ }
+});
+
+
+var all_columns = [TLSColumn, IconColumn, PathColumn, MethodColumn, StatusColumn, TimeColumn];
+
diff --git a/web/src/js/components/flowtable.jsx.js b/web/src/js/components/flowtable.jsx.js
index 39721baf..b1b6fa98 100644
--- a/web/src/js/components/flowtable.jsx.js
+++ b/web/src/js/components/flowtable.jsx.js
@@ -9,7 +9,14 @@ var FlowRow = React.createClass({
flow: flow
});
}.bind(this));
- return <tr onClick={this.props.onClick} >{columns}</tr>;
+ var className = "";
+ if(this.props.selected){
+ className += "selected";
+ }
+ return (
+ <tr className={className} onClick={this.props.selectFlow.bind(null, flow)}>
+ {columns}
+ </tr>);
}
});
@@ -25,107 +32,20 @@ var FlowTableHead = React.createClass({
var FlowTableBody = React.createClass({
render: function(){
var rows = this.props.flows.map(function(flow){
- //TODO: Add UUID
- return <FlowRow onClick={this.props.onClick} flow={flow} columns={this.props.columns}/>;
+ 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 TLSColumn = React.createClass({
- statics: {
- renderTitle: function(){
- return <th key="tls" className="col-tls"></th>;
- }
- },
- render: function(){
- var flow = this.props.flow;
- var ssl = (flow.request.scheme == "https");
- return <td className={ssl ? "col-tls-https" : "col-tls-http"}></td>;
- }
-});
-
-
-var IconColumn = React.createClass({
- statics: {
- renderTitle: function(){
- return <th key="icon" className="col-icon"></th>;
- }
- },
- render: function(){
- var flow = this.props.flow;
- return <td className="resource-icon resource-icon-plain"></td>;
- }
-});
-
-var PathColumn = React.createClass({
- statics: {
- renderTitle: function(){
- return <th key="path" className="col-path">Path</th>;
- }
- },
- render: function(){
- var flow = this.props.flow;
- return <td>{flow.request.scheme + "://" + flow.request.host + flow.request.path}</td>;
- }
-});
-
-
-var MethodColumn = React.createClass({
- statics: {
- renderTitle: function(){
- return <th key="method" className="col-method">Method</th>;
- }
- },
- render: function(){
- var flow = this.props.flow;
- return <td>{flow.request.method}</td>;
+ return <tbody onKeyDown={this.props.onKeyDown} tabIndex="0">{rows}</tbody>;
}
});
-var StatusColumn = React.createClass({
- statics: {
- renderTitle: function(){
- return <th key="status" className="col-status">Status</th>;
- }
- },
- render: function(){
- var flow = this.props.flow;
- var status;
- if(flow.response){
- status = flow.response.code;
- } else {
- status = null;
- }
- return <td>{status}</td>;
- }
-});
-
-
-var TimeColumn = React.createClass({
- statics: {
- renderTitle: function(){
- return <th key="time" className="col-time">Time</th>;
- }
- },
- render: function(){
- var flow = this.props.flow;
- var time;
- if(flow.response){
- time = Math.round(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))+"ms";
- } else {
- time = "...";
- }
- return <td>{time}</td>;
- }
-});
-
-
-var all_columns = [TLSColumn, IconColumn, PathColumn, MethodColumn, StatusColumn, TimeColumn];
-
-
var FlowTable = React.createClass({
getInitialState: function () {
return {
@@ -146,18 +66,88 @@ var FlowTable = React.createClass({
flows: this.flowStore.getAll()
});
},
- onClick: function(e){
- console.log("rowclick", e);
+ selectFlow: function(flow){
+ this.setState({
+ selected: 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;
+ }
+ },
+ selectRowRelative: function(i){
+ var index;
+ if(!this.state.selected){
+ if(i > 0){
+ index = this.flows.length-1;
+ } else {
+ index = 0;
+ }
+ } else {
+ index = _.findIndex(this.state.flows, function(f){
+ return f === this.state.selected;
+ }.bind(this));
+ index = Math.min(Math.max(0, index+i), this.state.flows.length-1);
+ }
+ this.selectFlow(this.state.flows[index]);
+ },
+ onKeyDown: function(e){
+ switch(e.keyCode){
+ case Key.DOWN:
+ this.selectRowRelative(+1);
+ return false;
+ break;
+ case Key.UP:
+ this.selectRowRelative(-1);
+ return false;
+ break;
+ case Key.ENTER:
+ console.log("Open details pane...", this.state.selected);
+ break;
+ case Key.ESC:
+ console.log("")
+ default:
+ console.debug("keydown", e.keyCode);
+ return;
+ }
+ return false;
+ },
+ onScroll: function(e){
+ //Abusing CSS transforms to set thead into position:fixed.
+ var head = this.refs.head.getDOMNode();
+ head.style.transform = "translate(0,"+this.getDOMNode().scrollTop+"px)";
},
render: function () {
var flows = this.state.flows.map(function(flow){
return <div>{flow.request.method} {flow.request.scheme}://{flow.request.host}{flow.request.path}</div>;
});
return (
+ <main onScroll={this.onScroll}>
<table className="flow-table">
- <FlowTableHead columns={this.state.columns}/>
- <FlowTableBody onClick={this.onClick} columns={this.state.columns} flows={this.state.flows}/>
+ <FlowTableHead ref="head"
+ columns={this.state.columns}/>
+ <FlowTableBody ref="body"
+ selectFlow={this.selectFlow}
+ onKeyDown={this.onKeyDown}
+ selected={this.state.selected}
+ columns={this.state.columns}
+ flows={this.state.flows}/>
</table>
+ </main>
);
}
});
diff --git a/web/src/js/components/proxyapp.jsx.js b/web/src/js/components/proxyapp.jsx.js
index 63998ffe..486e723f 100644
--- a/web/src/js/components/proxyapp.jsx.js
+++ b/web/src/js/components/proxyapp.jsx.js
@@ -26,7 +26,7 @@ var ProxyAppMain = React.createClass({
return (
<div id="container">
<Header settings={this.state.settings}/>
- <div id="main"><this.props.activeRouteHandler/></div>
+ <this.props.activeRouteHandler/>
{this.state.settings.showEventLog ? <EventLog/> : null}
<Footer settings={this.state.settings}/>
</div>
diff --git a/web/src/js/stores/flowstore.js b/web/src/js/stores/flowstore.js
index a5cb74ba..ba5b0788 100644
--- a/web/src/js/stores/flowstore.js
+++ b/web/src/js/stores/flowstore.js
@@ -35,9 +35,8 @@ _.extend(FlowView.prototype, EventEmitter.prototype, {
this.emit("change");
},
_update: function(flow){
- console.debug("FIXME: Use UUID");
var idx = _.findIndex(this.flows, function(f){
- return flow.request.timestamp_start == f.request.timestamp_start;
+ return flow.id === f.id;
});
if(idx < 0){
@@ -61,7 +60,13 @@ _.extend(_FlowStore.prototype, EventEmitter.prototype, {
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;
diff --git a/web/src/js/utils.js b/web/src/js/utils.js
index 39ad92fb..6e545d8a 100644
--- a/web/src/js/utils.js
+++ b/web/src/js/utils.js
@@ -11,3 +11,13 @@ var AutoScrollMixin = {
}
},
};
+
+
+var Key = {
+ UP: 38,
+ DOWN: 40,
+ LEFT: 37,
+ RIGHT: 39,
+ ENTER: 13,
+ ESC: 27
+} \ No newline at end of file