diff options
author | Maximilian Hils <git@maximilianhils.com> | 2014-09-18 02:22:10 +0200 |
---|---|---|
committer | Maximilian Hils <git@maximilianhils.com> | 2014-09-18 02:22:10 +0200 |
commit | 0d64cc93278d39bd4c87cf5110d326f57574c8a1 (patch) | |
tree | 41cdb8ea3fdd51304c23234ca8fd7c2372d96988 /web | |
parent | 6a161be6b4c526fcc5f6581c7faff00a2c976f37 (diff) | |
download | mitmproxy-0d64cc93278d39bd4c87cf5110d326f57574c8a1.tar.gz mitmproxy-0d64cc93278d39bd4c87cf5110d326f57574c8a1.tar.bz2 mitmproxy-0d64cc93278d39bd4c87cf5110d326f57574c8a1.zip |
flowtable: add selection indicator, add keyboard navigation
Diffstat (limited to 'web')
-rw-r--r-- | web/gulpfile.js | 1 | ||||
-rw-r--r-- | web/src/css/flowtable.less | 5 | ||||
-rw-r--r-- | web/src/css/layout.less | 2 | ||||
-rw-r--r-- | web/src/js/components/flowtable-columns.jsx.js | 95 | ||||
-rw-r--r-- | web/src/js/components/flowtable.jsx.js | 192 | ||||
-rw-r--r-- | web/src/js/components/proxyapp.jsx.js | 2 | ||||
-rw-r--r-- | web/src/js/stores/flowstore.js | 9 | ||||
-rw-r--r-- | web/src/js/utils.js | 10 |
8 files changed, 209 insertions, 107 deletions
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 |