aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMaximilian Hils <git@maximilianhils.com>2014-09-18 21:13:50 +0200
committerMaximilian Hils <git@maximilianhils.com>2014-09-18 21:13:50 +0200
commitd1ba150ea79689a55898efa760f7d77ca5ed601c (patch)
tree3b92ea9bae396fe1ab0b60310f4aa473c1194d0f
parent01da54f1c306a5d595046bd39bf2be8bbc86c132 (diff)
downloadmitmproxy-d1ba150ea79689a55898efa760f7d77ca5ed601c.tar.gz
mitmproxy-d1ba150ea79689a55898efa760f7d77ca5ed601c.tar.bz2
mitmproxy-d1ba150ea79689a55898efa760f7d77ca5ed601c.zip
web: detailpane impl
-rw-r--r--libmproxy/web/static/css/app.css156
-rw-r--r--libmproxy/web/static/js/app.js338
-rw-r--r--web/gulpfile.js2
-rw-r--r--web/src/css/app.less2
-rw-r--r--web/src/css/flowdetail.less7
-rw-r--r--web/src/css/flowtable.less26
-rw-r--r--web/src/css/header.less16
-rw-r--r--web/src/css/layout.less16
-rw-r--r--web/src/css/tabs.less45
-rw-r--r--web/src/js/components/flowdetail.jsx.js62
-rw-r--r--web/src/js/components/flowtable-columns.jsx.js17
-rw-r--r--web/src/js/components/flowtable.jsx.js88
-rw-r--r--web/src/js/components/header.jsx.js68
-rw-r--r--web/src/js/components/mainview.jsx.js69
-rw-r--r--web/src/js/components/proxyapp.jsx.js25
-rw-r--r--web/src/js/stores/flowstore.js4
-rw-r--r--web/src/js/utils.js12
17 files changed, 688 insertions, 265 deletions
diff --git a/libmproxy/web/static/css/app.css b/libmproxy/web/static/css/app.css
index 673beb3f..e7564ee8 100644
--- a/libmproxy/web/static/css/app.css
+++ b/libmproxy/web/static/css/app.css
@@ -1,10 +1,12 @@
html {
- box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
}
*,
*:before,
*:after {
- box-sizing: inherit;
+ -moz-box-sizing: inherit;
+ box-sizing: inherit;
}
.resource-icon {
width: 32px;
@@ -48,65 +50,134 @@ body,
overflow: hidden;
}
#container {
+ display: -webkit-flex;
+ display: -ms-flexbox;
display: flex;
- flex-direction: column;
+ -webkit-flex-direction: column;
+ -ms-flex-direction: column;
+ flex-direction: column;
}
#container > header,
#container > footer,
#container > .eventlog {
- flex: 0 0 auto;
-}
-main {
- flex: 1 1 auto;
- overflow: auto;
-}
-header {
- background-color: white;
-}
-header .title-bar {
- line-height: 25px;
- text-align: center;
-}
-header nav {
+ -webkit-flex: 0 0 auto;
+ -ms-flex: 0 0 auto;
+ flex: 0 0 auto;
+}
+.main-view {
+ -webkit-flex: 1 1 auto;
+ -ms-flex: 1 1 auto;
+ flex: 1 1 auto;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+}
+.main-view.vertical {
+ -webkit-flex-direction: column;
+ -ms-flex-direction: column;
+ flex-direction: column;
+}
+.main-view .flow-detail,
+.main-view .flow-table {
+ -webkit-flex: 1 1 auto;
+ -ms-flex: 1 1 auto;
+ flex: 1 1 auto;
+ -webkit-flex-basis: 50%;
+ -ms-flex-preferred-size: 50%;
+ flex-basis: 50%;
+}
+.nav-tabs {
border-bottom: solid #a6a6a6 1px;
}
-header nav a {
+.nav-tabs a {
display: inline-block;
- padding: 3px 14px;
- margin: 0 2px -1px;
border: solid transparent 1px;
+ text-decoration: none;
}
-header nav a.active {
+.nav-tabs a.active {
+ background-color: white;
border-color: #a6a6a6;
border-bottom-color: white;
}
-header nav a:hover {
- /*
- @preview: lightgrey;
- border-top-color: @preview;
- border-left-color: @preview;
- border-right-color: @preview;
- */
- text-decoration: none;
-}
-header nav a.special {
+.nav-tabs a.special {
color: white;
background-color: #396cad;
border-bottom-color: #396cad;
}
-header nav a.special:hover {
+.nav-tabs a.special:hover {
background-color: #5386c6;
}
+.nav-tabs-lg a {
+ padding: 3px 14px;
+ margin: 0 2px -1px;
+}
+.nav-tabs-sm a {
+ padding: 0px 7px;
+ margin: 2px 2px -1px;
+}
+header {
+ background-color: white;
+ /*
+ nav {
+ border-bottom: solid @separator-color 1px;
+
+ a {
+ display: inline-block;
+ padding: 3px 14px;
+ margin: 0 2px -1px;
+ border: solid transparent 1px;
+ //text-transform: uppercase;
+ //font-family: Lato;
+
+ &.active {
+ border-color: @separator-color;
+ border-bottom-color: white;
+ }
+ &.active, &:hover {
+ text-decoration: none;
+ }
+ &.special {
+ @special-color: #396cad;
+ color: white;
+ background-color: @special-color;
+ border-bottom-color: @special-color;
+ &:hover {
+ background-color: lighten(@special-color, 10%);
+ }
+ }
+ }
+ }
+*/
+}
+header .title-bar {
+ line-height: 25px;
+ text-align: center;
+}
header .menu {
padding: 10px;
border-bottom: solid #a6a6a6 1px;
}
.flow-table {
width: 100%;
+ overflow: auto;
+}
+.flow-table table {
+ width: 100%;
table-layout: fixed;
}
.flow-table thead {
- background-color: #dadada;
+ background-color: #F2F2F2;
+ line-height: 23px;
+}
+.flow-table th {
+ font-weight: normal;
+ box-shadow: 0 1px 0 #a6a6a6;
+}
+.flow-table tbody {
+ outline: 0;
}
.flow-table tr {
cursor: pointer;
@@ -138,10 +209,21 @@ header .menu {
width: 50px;
}
.flow-table .col-time {
- width: 120px;
+ width: 50px;
+}
+.flow-table td.col-time {
+ text-align: right;
+}
+.flow-detail {
+ overflow: auto;
+}
+.flow-detail nav {
+ background-color: #F2F2F2;
}
.eventlog {
- flex: 0 0 auto;
+ -webkit-flex: 0 0 auto;
+ -ms-flex: 0 0 auto;
+ flex: 0 0 auto;
margin: 0;
border-radius: 0;
height: 200px;
@@ -151,6 +233,4 @@ header .menu {
footer {
box-shadow: 0 -1px 3px #d3d3d3;
padding: 0px 10px 3px;
-}
-
-/*# sourceMappingURL=../css/app.css.map */ \ No newline at end of file
+} \ No newline at end of file
diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js
index 08d02cc9..5e176090 100644
--- a/libmproxy/web/static/js/app.js
+++ b/libmproxy/web/static/js/app.js
@@ -12,15 +12,25 @@ var AutoScrollMixin = {
},
};
+var StickyHeadMixin = {
+ adjustHead: function(){
+ // Abusing CSS transforms to set the element
+ // referenced as head into some kind of position:sticky.
+ var head = this.refs.head.getDOMNode();
+ head.style.transform = "translate(0,"+this.getDOMNode().scrollTop+"px)";
+ }
+};
var Key = {
UP: 38,
DOWN: 40,
+ PAGE_UP: 33,
+ PAGE_DOWN: 34,
LEFT: 37,
RIGHT: 39,
ENTER: 13,
ESC: 27
-}
+};
const PayloadSources = {
VIEW: "view",
SERVER: "server"
@@ -311,8 +321,8 @@ _.extend(_FlowStore.prototype, EventEmitter.prototype, {
flows = flows.concat(_.cloneDeep(flows)).concat(_.cloneDeep(flows));
var id = 1;
flows.forEach(function(flow){
- flow.id = "uuid-"+id++;
- })
+ flow.id = "uuid-" + id++;
+ });
view.add_bulk(flows);
});
@@ -372,6 +382,10 @@ var Connection = new _Connection(location.origin + "/updates");
/** @jsx React.DOM */
var MainMenu = React.createClass({displayName: 'MainMenu',
+ statics: {
+ title: "Traffic",
+ route: "flows"
+ },
toggleEventLog: function () {
SettingsActions.update({
showEventLog: !this.props.settings.showEventLog
@@ -387,72 +401,74 @@ var MainMenu = React.createClass({displayName: 'MainMenu',
);
}
});
+
+
var ToolsMenu = React.createClass({displayName: 'ToolsMenu',
+ statics: {
+ title: "Tools",
+ route: "flows"
+ },
render: function () {
return React.DOM.div(null, "Tools Menu");
}
});
+
+
var ReportsMenu = React.createClass({displayName: 'ReportsMenu',
+ statics: {
+ title: "Visualization",
+ route: "reports"
+ },
render: function () {
return React.DOM.div(null, "Reports Menu");
}
});
-var _Header_Entries = {
- main: {
- title: "Traffic",
- route: "main",
- menu: MainMenu
- },
- tools: {
- title: "Tools",
- route: "main",
- menu: ToolsMenu
- },
- reports: {
- title: "Visualization",
- route: "reports",
- menu: ReportsMenu
- }
-};
+var header_entries = [MainMenu, ToolsMenu, ReportsMenu];
+
var Header = React.createClass({displayName: 'Header',
getInitialState: function () {
return {
- active: "main"
+ active: header_entries[0]
};
},
handleClick: function (active) {
+ ReactRouter.transitionTo(active.route);
this.setState({active: active});
- ReactRouter.transitionTo(_Header_Entries[active].route);
return false;
},
handleFileClick: function () {
console.log("File click");
},
render: function () {
- var header = [];
- for (var item in _Header_Entries) {
- var classes = this.state.active == item ? "active" : "";
- header.push(React.DOM.a({key: item, href: "#", className: classes,
- onClick: this.handleClick.bind(this, item)}, _Header_Entries[item].title));
- }
-
- var menu = _Header_Entries[this.state.active].menu({
- settings: this.props.settings
- });
+ var header = header_entries.map(function(entry){
+ var classes = React.addons.classSet({
+ active: entry == this.state.active
+ });
+ return (
+ React.DOM.a({key: entry.title,
+ href: "#",
+ className: classes,
+ onClick: this.handleClick.bind(this, entry)
+ },
+ entry.title
+ )
+ );
+ }.bind(this));
+
return (
React.DOM.header(null,
React.DOM.div({className: "title-bar"},
"mitmproxy ", this.props.settings.version
),
- React.DOM.nav(null,
+ React.DOM.nav({className: "nav-tabs nav-tabs-lg"},
React.DOM.a({href: "#", className: "special", onClick: this.handleFileClick}, " File "),
header
),
React.DOM.div({className: "menu"},
- menu
+ this.state.active({settings: this.props.settings})
)
)
);
@@ -471,7 +487,12 @@ var TLSColumn = React.createClass({displayName: 'TLSColumn',
render: function(){
var flow = this.props.flow;
var ssl = (flow.request.scheme == "https");
- return React.DOM.td({className: ssl ? "col-tls-https" : "col-tls-http"});
+ var classes = React.addons.classSet({
+ "col-tls": true,
+ "col-tls-https": ssl,
+ "col-tls-http": !ssl
+ });
+ return React.DOM.td({className: classes});
}
});
@@ -484,7 +505,7 @@ var IconColumn = React.createClass({displayName: 'IconColumn',
},
render: function(){
var flow = this.props.flow;
- return React.DOM.td({className: "resource-icon resource-icon-plain"});
+ return React.DOM.td({className: "col-icon"}, React.DOM.div({className: "resource-icon resource-icon-plain"}));
}
});
@@ -496,7 +517,7 @@ var PathColumn = React.createClass({displayName: 'PathColumn',
},
render: function(){
var flow = this.props.flow;
- return React.DOM.td(null, flow.request.scheme + "://" + flow.request.host + flow.request.path);
+ return React.DOM.td({className: "col-path"}, flow.request.scheme + "://" + flow.request.host + flow.request.path);
}
});
@@ -509,7 +530,7 @@ var MethodColumn = React.createClass({displayName: 'MethodColumn',
},
render: function(){
var flow = this.props.flow;
- return React.DOM.td(null, flow.request.method);
+ return React.DOM.td({className: "col-method"}, flow.request.method);
}
});
@@ -528,7 +549,7 @@ var StatusColumn = React.createClass({displayName: 'StatusColumn',
} else {
status = null;
}
- return React.DOM.td(null, status);
+ return React.DOM.td({className: "col-status"}, status);
}
});
@@ -547,7 +568,7 @@ var TimeColumn = React.createClass({displayName: 'TimeColumn',
} else {
time = "...";
}
- return React.DOM.td(null, time);
+ return React.DOM.td({className: "col-time"}, time);
}
});
@@ -561,10 +582,7 @@ 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
- });
+ return column({key: column.displayName, flow: flow});
}.bind(this));
var className = "";
if(this.props.selected){
@@ -604,30 +622,13 @@ var FlowTableBody = React.createClass({displayName: 'FlowTableBody',
var FlowTable = React.createClass({displayName: 'FlowTable',
+ mixins: [StickyHeadMixin, AutoScrollMixin],
getInitialState: function () {
return {
- flows: [],
columns: all_columns
};
},
- componentDidMount: function () {
- this.flowStore = FlowStore.getView();
- this.flowStore.addListener("change",this.onFlowChange);
- },
- componentWillUnmount: function () {
- this.flowStore.removeListener("change",this.onFlowChange);
- this.flowStore.close();
- },
- onFlowChange: function () {
- this.setState({
- flows: this.flowStore.getAll()
- });
- },
- selectFlow: function(flow){
- this.setState({
- selected: flow
- });
-
+ 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();
@@ -646,71 +647,196 @@ var FlowTable = React.createClass({displayName: 'FlowTable',
viewport.scrollTop = flowNode_bottom - viewport.offsetHeight;
}
},
- selectRowRelative: function(i){
+ selectFlowRelative: function(i){
var index;
- if(!this.state.selected){
+ if(!this.props.selected){
if(i > 0){
- index = this.flows.length-1;
+ index = this.props.flows.length-1;
} else {
index = 0;
}
} else {
- index = _.findIndex(this.state.flows, function(f){
- return f === this.state.selected;
+ index = _.findIndex(this.props.flows, function(f){
+ return f === this.props.selected;
}.bind(this));
- index = Math.min(Math.max(0, index+i), this.state.flows.length-1);
+ index = Math.min(Math.max(0, index+i), this.props.flows.length-1);
}
- this.selectFlow(this.state.flows[index]);
+ this.props.selectFlow(this.props.flows[index]);
},
onKeyDown: function(e){
switch(e.keyCode){
case Key.DOWN:
- this.selectRowRelative(+1);
- return false;
+ this.selectFlowRelative(+1);
break;
case Key.UP:
- this.selectRowRelative(-1);
- return false;
+ this.selectFlowRelative(-1);
+ break;
+ case Key.PAGE_DOWN:
+ this.selectFlowRelative(+10);
break;
- case Key.ENTER:
- console.log("Open details pane...", this.state.selected);
+ case Key.PAGE_UP:
+ this.selectFlowRelative(-10);
break;
case Key.ESC:
- console.log("")
+ this.props.selectFlow(null);
+ break;
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({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})
+ React.DOM.div({className: "flow-table", onScroll: this.adjustHead},
+ React.DOM.table(null,
+ 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,
+ onKeyDown: this.onKeyDown})
+ )
)
- )
);
}
});
/** @jsx React.DOM */
+var FlowDetailNav = React.createClass({displayName: 'FlowDetailNav',
+ render: function(){
+
+ var items = ["request", "response", "details"].map(function(e){
+ var str = e.charAt(0).toUpperCase() + e.slice(1);
+ var className = this.props.active === e ? "active" : "";
+ var onClick = function(){
+ this.props.selectTab(e);
+ return false;
+ }.bind(this);
+ return React.DOM.a({key: e,
+ href: "#",
+ className: className,
+ onClick: onClick}, str);
+ }.bind(this));
+ return (
+ React.DOM.nav({ref: "head", className: "nav-tabs nav-tabs-sm"},
+ items
+ )
+ );
+ }
+});
+
+var FlowDetailRequest = React.createClass({displayName: 'FlowDetailRequest',
+ render: function(){
+ return React.DOM.div(null, "request");
+ }
+});
+
+var FlowDetailResponse = React.createClass({displayName: 'FlowDetailResponse',
+ render: function(){
+ return React.DOM.div(null, "response");
+ }
+});
+
+var FlowDetailConnectionInfo = React.createClass({displayName: 'FlowDetailConnectionInfo',
+ render: function(){
+ return React.DOM.div(null, "details");
+ }
+})
+
+var tabs = {
+ request: FlowDetailRequest,
+ response: FlowDetailResponse,
+ details: FlowDetailConnectionInfo
+}
+
+var FlowDetail = React.createClass({displayName: 'FlowDetail',
+ mixins: [StickyHeadMixin],
+ render: function(){
+ var flow = JSON.stringify(this.props.flow, null, 2);
+ var Tab = tabs[this.props.active];
+ return (
+ React.DOM.div({className: "flow-detail", onScroll: this.adjustHead},
+ FlowDetailNav({active: this.props.active, selectTab: this.props.selectTab}),
+ Tab(null)
+ )
+ );
+ }
+});
+/** @jsx React.DOM */
+
+var MainView = React.createClass({displayName: 'MainView',
+ getInitialState: function() {
+ return {
+ flows: [],
+ };
+ },
+ componentDidMount: function () {
+ console.log("get view");
+ this.flowStore = FlowStore.getView();
+ this.flowStore.addListener("change",this.onFlowChange);
+ },
+ componentWillUnmount: function () {
+ this.flowStore.removeListener("change",this.onFlowChange);
+ this.flowStore.close();
+ },
+ onFlowChange: function () {
+ this.setState({
+ flows: this.flowStore.getAll()
+ });
+ },
+ selectFlow: function(flow) {
+ if(flow){
+ ReactRouter.replaceWith(
+ "flow",
+ {
+ flowId: flow.id,
+ detailTab: this.props.params.detailTab || "request"
+ }
+ );
+ this.refs.flowTable.scrollIntoView(flow);
+ } else {
+ ReactRouter.replaceWith("flows");
+ }
+ },
+ selectDetailTab: function(panel) {
+ ReactRouter.replaceWith(
+ "flow",
+ {
+ flowId: this.props.params.flowId,
+ detailTab: panel
+ }
+ );
+ },
+ render: function() {
+ var selected = _.find(this.state.flows, { id: this.props.params.flowId });
+
+ var details = null;
+ if(selected){
+ details = (
+ FlowDetail({ref: "flowDetails",
+ flow: selected,
+ selectTab: this.selectDetailTab,
+ active: this.props.params.detailTab})
+ );
+ }
+
+ return (
+ React.DOM.div({className: "main-view"},
+ FlowTable({ref: "flowTable",
+ flows: this.state.flows,
+ selectFlow: this.selectFlow,
+ selected: selected}),
+ details
+ )
+ );
+ }
+});
+/** @jsx React.DOM */
+
var EventLog = React.createClass({displayName: 'EventLog',
mixins:[AutoScrollMixin],
getInitialState: function () {
@@ -791,7 +917,7 @@ var ProxyAppMain = React.createClass({displayName: 'ProxyAppMain',
return (
React.DOM.div({id: "container"},
Header({settings: this.state.settings}),
- this.props.activeRouteHandler(null),
+ this.props.activeRouteHandler({settings: this.state.settings}),
this.state.settings.showEventLog ? EventLog(null) : null,
Footer({settings: this.state.settings})
)
@@ -800,16 +926,22 @@ var ProxyAppMain = React.createClass({displayName: 'ProxyAppMain',
});
+var Routes = ReactRouter.Routes;
+var Route = ReactRouter.Route;
+var Redirect = ReactRouter.Redirect;
+var DefaultRoute = ReactRouter.DefaultRoute;
+var NotFoundRoute = ReactRouter.NotFoundRoute;
+
+
var ProxyApp = (
- ReactRouter.Routes({location: "hash"},
- ReactRouter.Route({name: "app", path: "/", handler: ProxyAppMain},
- ReactRouter.Route({name: "main", handler: FlowTable}),
- ReactRouter.Route({name: "reports", handler: Reports}),
- ReactRouter.Redirect({to: "main"})
+ Routes({location: "hash"},
+ Route({path: "/", handler: ProxyAppMain},
+ Route({name: "flows", path: "flows", handler: MainView}),
+ Route({name: "flow", path: "flows/:flowId/:detailTab", handler: MainView}),
+ Route({name: "reports", handler: Reports})
)
)
);
-
$(function () {
Connection.init();
app = React.renderComponent(ProxyApp, document.body);
diff --git a/web/gulpfile.js b/web/gulpfile.js
index a0f7825b..fc78bd1f 100644
--- a/web/gulpfile.js
+++ b/web/gulpfile.js
@@ -44,6 +44,8 @@ var path = {
'js/components/header.jsx.js',
'js/components/flowtable-columns.jsx.js',
'js/components/flowtable.jsx.js',
+ 'js/components/flowdetail.jsx.js',
+ 'js/components/mainview.jsx.js',
'js/components/eventlog.jsx.js',
'js/components/footer.jsx.js',
'js/components/proxyapp.jsx.js',
diff --git a/web/src/css/app.less b/web/src/css/app.less
index cc65cfdd..26f22572 100644
--- a/web/src/css/app.less
+++ b/web/src/css/app.less
@@ -9,7 +9,9 @@ html {
@import (less) "sprites.less";
@import (less) "layout.less";
+@import (less) "tabs.less";
@import (less) "header.less";
@import (less) "flowtable.less";
+@import (less) "flowdetail.less";
@import (less) "eventlog.less";
@import (less) "footer.less"; \ No newline at end of file
diff --git a/web/src/css/flowdetail.less b/web/src/css/flowdetail.less
new file mode 100644
index 00000000..19eeecc0
--- /dev/null
+++ b/web/src/css/flowdetail.less
@@ -0,0 +1,7 @@
+.flow-detail {
+ overflow: auto;
+
+ nav {
+ background-color: #F2F2F2;
+ }
+} \ No newline at end of file
diff --git a/web/src/css/flowtable.less b/web/src/css/flowtable.less
index d78fe31c..2a9c318b 100644
--- a/web/src/css/flowtable.less
+++ b/web/src/css/flowtable.less
@@ -1,9 +1,24 @@
.flow-table {
width: 100%;
- table-layout: fixed;
+ overflow: auto;
+
+ table {
+ width: 100%;
+ table-layout: fixed;
+ }
thead {
- background-color: #dadada;
+ background-color: #F2F2F2;
+ line-height: 23px;
+ }
+
+ th {
+ font-weight: normal;
+ box-shadow: 0 1px 0 #a6a6a6;
+ }
+
+ tbody {
+ outline: 0;
}
tr {
@@ -19,9 +34,7 @@
text-overflow: ellipsis;
}
- //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;
@@ -39,6 +52,9 @@
width: 50px;
}
.col-time {
- width: 120px;
+ width: 50px;
+ }
+ td.col-time {
+ text-align: right;
}
} \ No newline at end of file
diff --git a/web/src/css/header.less b/web/src/css/header.less
index 2a3c9765..66e8ac8a 100644
--- a/web/src/css/header.less
+++ b/web/src/css/header.less
@@ -7,7 +7,7 @@ header {
}
@separator-color: lighten(grey, 15%);
-
+/*
nav {
border-bottom: solid @separator-color 1px;
@@ -20,16 +20,10 @@ header {
//font-family: Lato;
&.active {
- border-color: @separator-color;
- border-bottom-color: white;
+ border-color: @separator-color;
+ border-bottom-color: white;
}
- &:hover {
- /*
- @preview: lightgrey;
- border-top-color: @preview;
- border-left-color: @preview;
- border-right-color: @preview;
- */
+ &.active, &:hover {
text-decoration: none;
}
&.special {
@@ -43,7 +37,7 @@ header {
}
}
}
-
+*/
.menu {
padding: 10px;
border-bottom: solid @separator-color 1px;
diff --git a/web/src/css/layout.less b/web/src/css/layout.less
index 320baee8..6e4abd24 100644
--- a/web/src/css/layout.less
+++ b/web/src/css/layout.less
@@ -13,7 +13,19 @@ html, body, #container {
}
}
-main {
+.main-view {
flex: 1 1 auto;
- overflow: auto;
+
+ display: flex;
+ flex-direction: row;
+
+ &.vertical {
+ flex-direction: column;
+ }
+
+ .flow-detail, .flow-table {
+ flex: 1 1 auto;
+ flex-basis: 50%;
+ }
+
} \ No newline at end of file
diff --git a/web/src/css/tabs.less b/web/src/css/tabs.less
new file mode 100644
index 00000000..36bc5b68
--- /dev/null
+++ b/web/src/css/tabs.less
@@ -0,0 +1,45 @@
+.nav-tabs {
+
+ @separator-color: lighten(grey, 15%);
+
+ border-bottom: solid @separator-color 1px;
+
+ a {
+ display: inline-block;
+ border: solid transparent 1px;
+ text-decoration: none;
+ //text-transform: uppercase;
+ //font-family: Lato;
+
+ &.active {
+ background-color: white;
+ border-color: @separator-color;
+ border-bottom-color: white;
+ }
+ &.special {
+ @special-color: #396cad;
+ color: white;
+ background-color: @special-color;
+ border-bottom-color: @special-color;
+ &:hover {
+ background-color: lighten(@special-color, 10%);
+ }
+ }
+ }
+
+}
+
+.nav-tabs-lg {
+ a {
+ padding: 3px 14px;
+ margin: 0 2px -1px;
+ }
+}
+
+.nav-tabs-sm {
+ a {
+ padding: 0px 7px;
+ margin: 2px 2px -1px;
+
+ }
+} \ No newline at end of file
diff --git a/web/src/js/components/flowdetail.jsx.js b/web/src/js/components/flowdetail.jsx.js
new file mode 100644
index 00000000..253084c2
--- /dev/null
+++ b/web/src/js/components/flowdetail.jsx.js
@@ -0,0 +1,62 @@
+/** @jsx React.DOM */
+
+var FlowDetailNav = React.createClass({
+ render: function(){
+
+ var items = ["request", "response", "details"].map(function(e){
+ var str = e.charAt(0).toUpperCase() + e.slice(1);
+ var className = this.props.active === e ? "active" : "";
+ var onClick = function(){
+ this.props.selectTab(e);
+ return false;
+ }.bind(this);
+ return <a key={e}
+ href="#"
+ className={className}
+ onClick={onClick}>{str}</a>;
+ }.bind(this));
+ return (
+ <nav ref="head" className="nav-tabs nav-tabs-sm">
+ {items}
+ </nav>
+ );
+ }
+});
+
+var FlowDetailRequest = React.createClass({
+ render: function(){
+ return <div>request</div>;
+ }
+});
+
+var FlowDetailResponse = React.createClass({
+ render: function(){
+ return <div>response</div>;
+ }
+});
+
+var FlowDetailConnectionInfo = React.createClass({
+ render: function(){
+ return <div>details</div>;
+ }
+})
+
+var tabs = {
+ request: FlowDetailRequest,
+ response: FlowDetailResponse,
+ details: FlowDetailConnectionInfo
+}
+
+var FlowDetail = React.createClass({
+ mixins: [StickyHeadMixin],
+ render: function(){
+ var flow = JSON.stringify(this.props.flow, null, 2);
+ var Tab = tabs[this.props.active];
+ return (
+ <div className="flow-detail" onScroll={this.adjustHead}>
+ <FlowDetailNav active={this.props.active} selectTab={this.props.selectTab}/>
+ <Tab/>
+ </div>
+ );
+ }
+}); \ 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
index e0cee365..0eb9966c 100644
--- a/web/src/js/components/flowtable-columns.jsx.js
+++ b/web/src/js/components/flowtable-columns.jsx.js
@@ -10,7 +10,12 @@ var TLSColumn = React.createClass({
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 classes = React.addons.classSet({
+ "col-tls": true,
+ "col-tls-https": ssl,
+ "col-tls-http": !ssl
+ });
+ return <td className={classes}></td>;
}
});
@@ -23,7 +28,7 @@ var IconColumn = React.createClass({
},
render: function(){
var flow = this.props.flow;
- return <td className="resource-icon resource-icon-plain"></td>;
+ return <td className="col-icon"><div className="resource-icon resource-icon-plain"></div></td>;
}
});
@@ -35,7 +40,7 @@ var PathColumn = React.createClass({
},
render: function(){
var flow = this.props.flow;
- return <td>{flow.request.scheme + "://" + flow.request.host + flow.request.path}</td>;
+ return <td className="col-path">{flow.request.scheme + "://" + flow.request.host + flow.request.path}</td>;
}
});
@@ -48,7 +53,7 @@ var MethodColumn = React.createClass({
},
render: function(){
var flow = this.props.flow;
- return <td>{flow.request.method}</td>;
+ return <td className="col-method">{flow.request.method}</td>;
}
});
@@ -67,7 +72,7 @@ var StatusColumn = React.createClass({
} else {
status = null;
}
- return <td>{status}</td>;
+ return <td className="col-status">{status}</td>;
}
});
@@ -86,7 +91,7 @@ var TimeColumn = React.createClass({
} else {
time = "...";
}
- return <td>{time}</td>;
+ return <td className="col-time">{time}</td>;
}
});
diff --git a/web/src/js/components/flowtable.jsx.js b/web/src/js/components/flowtable.jsx.js
index e0c285da..47576d70 100644
--- a/web/src/js/components/flowtable.jsx.js
+++ b/web/src/js/components/flowtable.jsx.js
@@ -4,10 +4,7 @@ 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
- });
+ return <column key={column.displayName} flow={flow}/>;
}.bind(this));
var className = "";
if(this.props.selected){
@@ -47,30 +44,13 @@ var FlowTableBody = React.createClass({
var FlowTable = React.createClass({
+ mixins: [StickyHeadMixin, AutoScrollMixin],
getInitialState: function () {
return {
- flows: [],
columns: all_columns
};
},
- componentDidMount: function () {
- this.flowStore = FlowStore.getView();
- this.flowStore.addListener("change",this.onFlowChange);
- },
- componentWillUnmount: function () {
- this.flowStore.removeListener("change",this.onFlowChange);
- this.flowStore.close();
- },
- onFlowChange: function () {
- this.setState({
- flows: this.flowStore.getAll()
- });
- },
- selectFlow: function(flow){
- this.setState({
- selected: flow
- });
-
+ 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();
@@ -89,65 +69,59 @@ var FlowTable = React.createClass({
viewport.scrollTop = flowNode_bottom - viewport.offsetHeight;
}
},
- selectRowRelative: function(i){
+ selectFlowRelative: function(i){
var index;
- if(!this.state.selected){
+ if(!this.props.selected){
if(i > 0){
- index = this.flows.length-1;
+ index = this.props.flows.length-1;
} else {
index = 0;
}
} else {
- index = _.findIndex(this.state.flows, function(f){
- return f === this.state.selected;
+ index = _.findIndex(this.props.flows, function(f){
+ return f === this.props.selected;
}.bind(this));
- index = Math.min(Math.max(0, index+i), this.state.flows.length-1);
+ index = Math.min(Math.max(0, index+i), this.props.flows.length-1);
}
- this.selectFlow(this.state.flows[index]);
+ this.props.selectFlow(this.props.flows[index]);
},
onKeyDown: function(e){
switch(e.keyCode){
case Key.DOWN:
- this.selectRowRelative(+1);
- return false;
+ this.selectFlowRelative(+1);
break;
case Key.UP:
- this.selectRowRelative(-1);
- return false;
+ this.selectFlowRelative(-1);
break;
- case Key.ENTER:
- console.log("Open details pane...", this.state.selected);
+ case Key.PAGE_DOWN:
+ this.selectFlowRelative(+10);
+ break;
+ case Key.PAGE_UP:
+ this.selectFlowRelative(-10);
break;
case Key.ESC:
- console.log("")
+ this.props.selectFlow(null);
+ break;
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 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>
+ <div className="flow-table" onScroll={this.adjustHead}>
+ <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}
+ onKeyDown={this.onKeyDown}/>
+ </table>
+ </div>
);
}
});
diff --git a/web/src/js/components/header.jsx.js b/web/src/js/components/header.jsx.js
index 8f613ff1..92a58282 100644
--- a/web/src/js/components/header.jsx.js
+++ b/web/src/js/components/header.jsx.js
@@ -1,6 +1,10 @@
/** @jsx React.DOM */
var MainMenu = React.createClass({
+ statics: {
+ title: "Traffic",
+ route: "flows"
+ },
toggleEventLog: function () {
SettingsActions.update({
showEventLog: !this.props.settings.showEventLog
@@ -16,72 +20,74 @@ var MainMenu = React.createClass({
);
}
});
+
+
var ToolsMenu = React.createClass({
+ statics: {
+ title: "Tools",
+ route: "flows"
+ },
render: function () {
return <div>Tools Menu</div>;
}
});
+
+
var ReportsMenu = React.createClass({
+ statics: {
+ title: "Visualization",
+ route: "reports"
+ },
render: function () {
return <div>Reports Menu</div>;
}
});
-var _Header_Entries = {
- main: {
- title: "Traffic",
- route: "main",
- menu: MainMenu
- },
- tools: {
- title: "Tools",
- route: "main",
- menu: ToolsMenu
- },
- reports: {
- title: "Visualization",
- route: "reports",
- menu: ReportsMenu
- }
-};
+var header_entries = [MainMenu, ToolsMenu, ReportsMenu];
+
var Header = React.createClass({
getInitialState: function () {
return {
- active: "main"
+ active: header_entries[0]
};
},
handleClick: function (active) {
+ ReactRouter.transitionTo(active.route);
this.setState({active: active});
- ReactRouter.transitionTo(_Header_Entries[active].route);
return false;
},
handleFileClick: function () {
console.log("File click");
},
render: function () {
- var header = [];
- for (var item in _Header_Entries) {
- var classes = this.state.active == item ? "active" : "";
- header.push(<a key={item} href="#" className={classes}
- onClick={this.handleClick.bind(this, item)}>{ _Header_Entries[item].title }</a>);
- }
-
- var menu = _Header_Entries[this.state.active].menu({
- settings: this.props.settings
- });
+ var header = header_entries.map(function(entry){
+ var classes = React.addons.classSet({
+ active: entry == this.state.active
+ });
+ return (
+ <a key={entry.title}
+ href="#"
+ className={classes}
+ onClick={this.handleClick.bind(this, entry)}
+ >
+ { entry.title}
+ </a>
+ );
+ }.bind(this));
+
return (
<header>
<div className="title-bar">
mitmproxy { this.props.settings.version }
</div>
- <nav>
+ <nav className="nav-tabs nav-tabs-lg">
<a href="#" className="special" onClick={this.handleFileClick}> File </a>
{header}
</nav>
<div className="menu">
- { menu }
+ <this.state.active settings={this.props.settings}/>
</div>
</header>
);
diff --git a/web/src/js/components/mainview.jsx.js b/web/src/js/components/mainview.jsx.js
new file mode 100644
index 00000000..10ecfed0
--- /dev/null
+++ b/web/src/js/components/mainview.jsx.js
@@ -0,0 +1,69 @@
+/** @jsx React.DOM */
+
+var MainView = React.createClass({
+ getInitialState: function() {
+ return {
+ flows: [],
+ };
+ },
+ componentDidMount: function () {
+ console.log("get view");
+ this.flowStore = FlowStore.getView();
+ this.flowStore.addListener("change",this.onFlowChange);
+ },
+ componentWillUnmount: function () {
+ this.flowStore.removeListener("change",this.onFlowChange);
+ this.flowStore.close();
+ },
+ onFlowChange: function () {
+ this.setState({
+ flows: this.flowStore.getAll()
+ });
+ },
+ selectFlow: function(flow) {
+ if(flow){
+ ReactRouter.replaceWith(
+ "flow",
+ {
+ flowId: flow.id,
+ detailTab: this.props.params.detailTab || "request"
+ }
+ );
+ this.refs.flowTable.scrollIntoView(flow);
+ } else {
+ ReactRouter.replaceWith("flows");
+ }
+ },
+ selectDetailTab: function(panel) {
+ ReactRouter.replaceWith(
+ "flow",
+ {
+ flowId: this.props.params.flowId,
+ detailTab: panel
+ }
+ );
+ },
+ render: function() {
+ var selected = _.find(this.state.flows, { id: this.props.params.flowId });
+
+ var details = null;
+ if(selected){
+ details = (
+ <FlowDetail ref="flowDetails"
+ flow={selected}
+ selectTab={this.selectDetailTab}
+ active={this.props.params.detailTab}/>
+ );
+ }
+
+ return (
+ <div className="main-view">
+ <FlowTable ref="flowTable"
+ flows={this.state.flows}
+ selectFlow={this.selectFlow}
+ selected={selected} />
+ {details}
+ </div>
+ );
+ }
+}); \ No newline at end of file
diff --git a/web/src/js/components/proxyapp.jsx.js b/web/src/js/components/proxyapp.jsx.js
index 486e723f..6895b852 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}/>
- <this.props.activeRouteHandler/>
+ <this.props.activeRouteHandler settings={this.state.settings}/>
{this.state.settings.showEventLog ? <EventLog/> : null}
<Footer settings={this.state.settings}/>
</div>
@@ -35,12 +35,19 @@ var ProxyAppMain = React.createClass({
});
+var Routes = ReactRouter.Routes;
+var Route = ReactRouter.Route;
+var Redirect = ReactRouter.Redirect;
+var DefaultRoute = ReactRouter.DefaultRoute;
+var NotFoundRoute = ReactRouter.NotFoundRoute;
+
+
var ProxyApp = (
- <ReactRouter.Routes location="hash">
- <ReactRouter.Route name="app" path="/" handler={ProxyAppMain}>
- <ReactRouter.Route name="main" handler={FlowTable}/>
- <ReactRouter.Route name="reports" handler={Reports}/>
- <ReactRouter.Redirect to="main"/>
- </ReactRouter.Route>
- </ReactRouter.Routes>
- );
+ <Routes location="hash">
+ <Route path="/" handler={ProxyAppMain}>
+ <Route name="flows" path="flows" handler={MainView}/>
+ <Route name="flow" path="flows/:flowId/:detailTab" handler={MainView}/>
+ <Route name="reports" handler={Reports}/>
+ </Route>
+ </Routes>
+ ); \ No newline at end of file
diff --git a/web/src/js/stores/flowstore.js b/web/src/js/stores/flowstore.js
index ba5b0788..c7623367 100644
--- a/web/src/js/stores/flowstore.js
+++ b/web/src/js/stores/flowstore.js
@@ -63,8 +63,8 @@ _.extend(_FlowStore.prototype, EventEmitter.prototype, {
flows = flows.concat(_.cloneDeep(flows)).concat(_.cloneDeep(flows));
var id = 1;
flows.forEach(function(flow){
- flow.id = "uuid-"+id++;
- })
+ flow.id = "uuid-" + id++;
+ });
view.add_bulk(flows);
});
diff --git a/web/src/js/utils.js b/web/src/js/utils.js
index 6e545d8a..947ad5c1 100644
--- a/web/src/js/utils.js
+++ b/web/src/js/utils.js
@@ -12,12 +12,22 @@ var AutoScrollMixin = {
},
};
+var StickyHeadMixin = {
+ adjustHead: function(){
+ // Abusing CSS transforms to set the element
+ // referenced as head into some kind of position:sticky.
+ var head = this.refs.head.getDOMNode();
+ head.style.transform = "translate(0,"+this.getDOMNode().scrollTop+"px)";
+ }
+};
var Key = {
UP: 38,
DOWN: 40,
+ PAGE_UP: 33,
+ PAGE_DOWN: 34,
LEFT: 37,
RIGHT: 39,
ENTER: 13,
ESC: 27
-} \ No newline at end of file
+}; \ No newline at end of file