aboutsummaryrefslogtreecommitdiffstats
path: root/libmproxy/web/static/js/app.js
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 /libmproxy/web/static/js/app.js
parent6a161be6b4c526fcc5f6581c7faff00a2c976f37 (diff)
downloadmitmproxy-0d64cc93278d39bd4c87cf5110d326f57574c8a1.tar.gz
mitmproxy-0d64cc93278d39bd4c87cf5110d326f57574c8a1.tar.bz2
mitmproxy-0d64cc93278d39bd4c87cf5110d326f57574c8a1.zip
flowtable: add selection indicator, add keyboard navigation
Diffstat (limited to 'libmproxy/web/static/js/app.js')
-rw-r--r--libmproxy/web/static/js/app.js178
1 files changed, 139 insertions, 39 deletions
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})
)