From d1ba150ea79689a55898efa760f7d77ca5ed601c Mon Sep 17 00:00:00 2001
From: Maximilian Hils <git@maximilianhils.com>
Date: Thu, 18 Sep 2014 21:13:50 +0200
Subject: web: detailpane impl

---
 web/gulpfile.js                                |  2 +
 web/src/css/app.less                           |  2 +
 web/src/css/flowdetail.less                    |  7 ++
 web/src/css/flowtable.less                     | 26 ++++++--
 web/src/css/header.less                        | 16 ++---
 web/src/css/layout.less                        | 16 ++++-
 web/src/css/tabs.less                          | 45 +++++++++++++
 web/src/js/components/flowdetail.jsx.js        | 62 ++++++++++++++++++
 web/src/js/components/flowtable-columns.jsx.js | 17 +++--
 web/src/js/components/flowtable.jsx.js         | 88 +++++++++-----------------
 web/src/js/components/header.jsx.js            | 68 +++++++++++---------
 web/src/js/components/mainview.jsx.js          | 69 ++++++++++++++++++++
 web/src/js/components/proxyapp.jsx.js          | 25 +++++---
 web/src/js/stores/flowstore.js                 |  4 +-
 web/src/js/utils.js                            | 12 +++-
 15 files changed, 335 insertions(+), 124 deletions(-)
 create mode 100644 web/src/css/flowdetail.less
 create mode 100644 web/src/css/tabs.less
 create mode 100644 web/src/js/components/flowdetail.jsx.js
 create mode 100644 web/src/js/components/mainview.jsx.js

(limited to 'web')

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
-- 
cgit v1.2.3