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 | |
parent | 6a161be6b4c526fcc5f6581c7faff00a2c976f37 (diff) | |
download | mitmproxy-0d64cc93278d39bd4c87cf5110d326f57574c8a1.tar.gz mitmproxy-0d64cc93278d39bd4c87cf5110d326f57574c8a1.tar.bz2 mitmproxy-0d64cc93278d39bd4c87cf5110d326f57574c8a1.zip |
flowtable: add selection indicator, add keyboard navigation
-rw-r--r-- | libmproxy/protocol/primitives.py | 3 | ||||
-rw-r--r-- | libmproxy/web/static/css/app.css | 7 | ||||
-rw-r--r-- | libmproxy/web/static/flows.json | 26 | ||||
-rw-r--r-- | libmproxy/web/static/js/app.js | 178 | ||||
-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 |
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 |