From ed8249023fb7c0d429b9278c63b51ac071700987 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 26 Nov 2014 04:18:21 +0100 Subject: introduce revised views, port over changes from multiple_views branch --- libmproxy/web/static/js/app.js | 294 +++++++++++++++++++++++++++-------------- 1 file changed, 194 insertions(+), 100 deletions(-) (limited to 'libmproxy/web/static/js/app.js') diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index fe317d7f..ddbb14f4 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -335,132 +335,216 @@ _.extend(_EventLogStore.prototype, EventEmitter.prototype, { var EventLogStore = new _EventLogStore(); AppDispatcher.register(EventLogStore.handle.bind(EventLogStore)); -function FlowView(store, live) { - EventEmitter.call(this); - this._store = store; - this.live = live; - this.flows = []; - - this.add = this.add.bind(this); - this.update = this.update.bind(this); - - if (live) { - this._store.addListener(ActionTypes.ADD_FLOW, this.add); - this._store.addListener(ActionTypes.UPDATE_FLOW, this.update); - } +function FlowStore(endpoint) { + this._views = []; + this.reset(); } - -_.extend(FlowView.prototype, EventEmitter.prototype, { - close: function () { - this._store.removeListener(ActionTypes.ADD_FLOW, this.add); - this._store.removeListener(ActionTypes.UPDATE_FLOW, this.update); +_.extend(FlowStore.prototype, { + add: function (flow) { + this._pos_map[flow.id] = this._flow_list.length; + this._flow_list.push(flow); + for (var i = 0; i < this._views.length; i++) { + this._views[i].add(flow); + } }, - getAll: function () { - return this.flows; + update: function (flow) { + this._flow_list[this._pos_map[flow.id]] = flow; + for (var i = 0; i < this._views.length; i++) { + this._views[i].update(flow); + } }, - add: function (flow) { - return this.update(flow); - }, - add_bulk: function (flows) { - //Treat all previously received updates as newer than the bulk update. - //If they weren't newer, we're about to receive an update for them very soon. - var updates = this.flows; - this.flows = flows; - updates.forEach(function(flow){ - this._update(flow); - }.bind(this)); - this.emit("change"); + remove: function (flow_id) { + this._flow_list.splice(this._pos_map[flow_id], 1); + this._build_map(); + for (var i = 0; i < this._views.length; i++) { + this._views[i].remove(flow_id); + } }, - _update: function(flow){ - var idx = _.findIndex(this.flows, function(f){ - return flow.id === f.id; - }); + reset: function (flows) { + this._flow_list = flows || []; + this._build_map(); + for (var i = 0; i < this._views.length; i++) { + this._views[i].recalculate(this._flow_list); + } + }, + _build_map: function () { + this._pos_map = {}; + for (var i = 0; i < this._flow_list.length; i++) { + var flow = this._flow_list[i]; + this._pos_map[flow.id] = i; + } + }, + open_view: function (filt, sort) { + var view = new FlowView(this._flow_list, filt, sort); + this._views.push(view); + return view; + }, + close_view: function (view) { + this._views = _.without(this._views, view); + } +}); + - if(idx < 0){ - this.flows.push(flow); - //if(this.flows.length > 100){ - // this.flows.shift(); - //} +function LiveFlowStore(endpoint) { + FlowStore.call(this); + this.updates_before_init = []; // (empty array is true in js) + this.endpoint = endpoint || "/flows"; + this.conn = new Connection(this.endpoint + "/updates"); + this.conn.onopen = this._onopen.bind(this); + this.conn.onmessage = function (e) { + var message = JSON.parse(e.data); + this.handle_update(message.type, message.data); + }.bind(this); +} +_.extend(LiveFlowStore.prototype, FlowStore.prototype, { + handle_update: function (type, data) { + console.log("LiveFlowStore.handle_update", type, data); + if (this.updates_before_init) { + console.log("defer update", type, data); + this.updates_before_init.push(arguments); } else { - this.flows[idx] = flow; + this[type](data); } }, - update: function(flow){ - this._update(flow); - this.emit("change"); + handle_fetch: function (data) { + console.log("Flows fetched."); + this.reset(data.flows); + var updates = this.updates_before_init; + this.updates_before_init = false; + for (var i = 0; i < updates.length; i++) { + this.handle_update.apply(this, updates[i]); + } + }, + _onopen: function () { + //Update stream openend, fetch list of flows. + console.log("Update Connection opened, fetching flows..."); + $.getJSON(this.endpoint, this.handle_fetch.bind(this)); }, }); +function SortByInsertionOrder() { + this.i = 0; + this.map = {}; + this.key = this.key.bind(this); +} +SortByInsertionOrder.prototype.key = function (flow) { + if (!(flow.id in this.map)) { + this.i++; + this.map[flow.id] = this.i; + } + return this.map[flow.id]; +}; -function _FlowStore() { +var default_sort = (new SortByInsertionOrder()).key; + +function FlowView(flows, filt, sort) { EventEmitter.call(this); + filt = filt || function (flow) { + return true; + }; + sort = sort || default_sort; + this.recalculate(flows, filt, sort); } -_.extend(_FlowStore.prototype, EventEmitter.prototype, { - getView: function (since) { - 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); - }); +_.extend(FlowView.prototype, EventEmitter.prototype, { + recalculate: function (flows, filt, sort) { + if (filt) { + this.filt = filt; + } + if (sort) { + this.sort = sort; + } + this.flows = flows.filter(this.filt); + this.flows.sort(function (a, b) { + return this.sort(a) - this.sort(b); + }.bind(this)); + this.emit("recalculate"); + }, + add: function (flow) { + if (this.filt(flow)) { + var idx = _.sortedIndex(this.flows, flow, this.sort); + if (idx === this.flows.length) { //happens often, .push is way faster. + this.flows.push(flow); + } else { + this.flows.splice(idx, 0, flow); + } + this.emit("add", flow, idx); + } + }, + update: function (flow) { + var idx; + var i = this.flows.length; + // Search from the back, we usually update the latest flows. + while (i--) { + if (this.flows[i].id === flow.id) { + idx = i; + break; + } + } - return view; + if (idx === -1) { //not contained in list + this.add(flow); + } else if (!this.filt(flow)) { + this.remove(flow.id); + } else { + if (this.sort(this.flows[idx]) !== this.sort(flow)) { //sortpos has changed + this.remove(this.flows[idx]); + this.add(flow); + } else { + this.flows[idx] = flow; + this.emit("update", flow, idx); + } + } }, - handle: function (action) { - switch (action.type) { - case ActionTypes.ADD_FLOW: - case ActionTypes.UPDATE_FLOW: - this.emit(action.type, action.data); + remove: function (flow_id) { + var i = this.flows.length; + while (i--) { + if (this.flows[i].id === flow_id) { + this.flows.splice(i, 1); + this.emit("remove", flow_id, i); break; - default: - return; + } } } }); - - -var FlowStore = new _FlowStore(); -AppDispatcher.register(FlowStore.handle.bind(FlowStore)); - -function _Connection(url) { - this.url = url; +function Connection(url) { + if(url[0] != "/"){ + this.url = url; + } else { + this.url = location.origin.replace("http", "ws") + url; + } + var ws = new WebSocket(this.url); + ws.onopen = function(){ + this.onopen.apply(this, arguments); + }.bind(this); + ws.onmessage = function(){ + this.onmessage.apply(this, arguments); + }.bind(this); + ws.onerror = function(){ + this.onerror.apply(this, arguments); + }.bind(this); + ws.onclose = function(){ + this.onclose.apply(this, arguments); + }.bind(this); + this.ws = ws; } -_Connection.prototype.init = function () { - this.openWebSocketConnection(); -}; -_Connection.prototype.openWebSocketConnection = function () { - this.ws = new WebSocket(this.url.replace("http", "ws")); - var ws = this.ws; - - ws.onopen = this.onopen.bind(this); - ws.onmessage = this.onmessage.bind(this); - ws.onerror = this.onerror.bind(this); - ws.onclose = this.onclose.bind(this); -}; -_Connection.prototype.onopen = function (open) { +Connection.prototype.onopen = function (open) { console.debug("onopen", this, arguments); }; -_Connection.prototype.onmessage = function (message) { - //AppDispatcher.dispatchServerAction(...); - var m = JSON.parse(message.data); - AppDispatcher.dispatchServerAction(m); +Connection.prototype.onmessage = function (message) { + console.warn("onmessage (not implemented)", this, message.data); }; -_Connection.prototype.onerror = function (error) { +Connection.prototype.onerror = function (error) { EventLogActions.add_event("WebSocket Connection Error."); console.debug("onerror", this, arguments); }; -_Connection.prototype.onclose = function (close) { +Connection.prototype.onclose = function (close) { EventLogActions.add_event("WebSocket Connection closed."); console.debug("onclose", this, arguments); }; - -var Connection = new _Connection(location.origin + "/updates"); - +Connection.prototype.close = function(){ + this.ws.close(); +}; /** @jsx React.DOM */ //React utils. For other utilities, see ../utils.js @@ -1214,8 +1298,14 @@ var MainView = React.createClass({displayName: 'MainView', }; }, componentDidMount: function () { - this.flowStore = FlowStore.getView(); - this.flowStore.addListener("change",this.onFlowChange); + //FIXME: The store should be global, move out of here. + window.flowstore = new LiveFlowStore(); + + this.flowStore = window.flowstore.open_view(); + this.flowStore.addListener("add",this.onFlowChange); + this.flowStore.addListener("update",this.onFlowChange); + this.flowStore.addListener("remove",this.onFlowChange); + this.flowStore.addListener("recalculate",this.onFlowChange); }, componentWillUnmount: function () { this.flowStore.removeListener("change",this.onFlowChange); @@ -1223,7 +1313,7 @@ var MainView = React.createClass({displayName: 'MainView', }, onFlowChange: function () { this.setState({ - flows: this.flowStore.getAll() + flows: this.flowStore.flows }); }, selectDetailTab: function(panel) { @@ -1518,7 +1608,11 @@ var ProxyApp = ( ) ); $(function () { - Connection.init(); - app = React.renderComponent(ProxyApp, document.body); + window.app = React.renderComponent(ProxyApp, document.body); + var UpdateConnection = new Connection("/updates"); + UpdateConnection.onmessage = function (message) { + var m = JSON.parse(message.data); + AppDispatcher.dispatchServerAction(m); + }; }); //# sourceMappingURL=app.js.map \ No newline at end of file -- cgit v1.2.3 From 38bf34ebd9826c6f69d97906282632fbd5cff06b Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 27 Nov 2014 01:38:30 +0100 Subject: web++ --- libmproxy/web/static/js/app.js | 541 +++++++++++++++++++++-------------------- 1 file changed, 284 insertions(+), 257 deletions(-) (limited to 'libmproxy/web/static/js/app.js') diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index ddbb14f4..d8d908e8 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -374,13 +374,8 @@ _.extend(FlowStore.prototype, { this._pos_map[flow.id] = i; } }, - open_view: function (filt, sort) { - var view = new FlowView(this._flow_list, filt, sort); - this._views.push(view); - return view; - }, - close_view: function (view) { - this._views = _.without(this._views, view); + get: function(flow_id){ + return this._flow_list[this._pos_map[flow_id]]; } }); @@ -397,6 +392,15 @@ function LiveFlowStore(endpoint) { }.bind(this); } _.extend(LiveFlowStore.prototype, FlowStore.prototype, { + close: function(){ + this.conn.close(); + }, + add: function(flow) { + // Make sure that deferred adds don't add an element twice. + if(!this._pos_map[flow.id]){ + FlowStore.prototype.add.call(this, flow); + } + }, handle_update: function (type, data) { console.log("LiveFlowStore.handle_update", type, data); if (this.updates_before_init) { @@ -437,16 +441,22 @@ SortByInsertionOrder.prototype.key = function (flow) { var default_sort = (new SortByInsertionOrder()).key; -function FlowView(flows, filt, sort) { +function FlowView(store, filt, sort) { EventEmitter.call(this); filt = filt || function (flow) { return true; }; sort = sort || default_sort; - this.recalculate(flows, filt, sort); + + this.store = store; + this.store._views.push(this); + this.recalculate(this.store._flow_list, filt, sort); } _.extend(FlowView.prototype, EventEmitter.prototype, { + close: function(){ + this.store._views = _.without(this.store._views, this); + }, recalculate: function (flows, filt, sort) { if (filt) { this.filt = filt; @@ -545,8 +555,6 @@ Connection.prototype.onclose = function (close) { Connection.prototype.close = function(){ this.ws.close(); }; -/** @jsx React.DOM */ - //React utils. For other utilities, see ../utils.js var Splitter = React.createClass({displayName: 'Splitter', @@ -639,14 +647,12 @@ var Splitter = React.createClass({displayName: 'Splitter', className += " splitter-y"; } return ( - React.DOM.div({className: className}, - React.DOM.div({onMouseDown: this.onMouseDown, draggable: "true"}) + React.createElement("div", {className: className}, + React.createElement("div", {onMouseDown: this.onMouseDown, draggable: "true"}) ) ); } }); -/** @jsx React.DOM */ - var MainMenu = React.createClass({displayName: 'MainMenu', statics: { title: "Traffic", @@ -659,9 +665,9 @@ var MainMenu = React.createClass({displayName: 'MainMenu', }, render: function () { return ( - React.DOM.div(null, - React.DOM.button({className: "btn " + (this.props.settings.showEventLog ? "btn-primary" : "btn-default"), onClick: this.toggleEventLog}, - React.DOM.i({className: "fa fa-database"}), " Display Event Log" + React.createElement("div", null, + React.createElement("button", {className: "btn " + (this.props.settings.showEventLog ? "btn-primary" : "btn-default"), onClick: this.toggleEventLog}, + React.createElement("i", {className: "fa fa-database"}), " Display Event Log" ) ) ); @@ -675,7 +681,7 @@ var ToolsMenu = React.createClass({displayName: 'ToolsMenu', route: "flows" }, render: function () { - return React.DOM.div(null, "Tools Menu"); + return React.createElement("div", null, "Tools Menu"); } }); @@ -686,7 +692,7 @@ var ReportsMenu = React.createClass({displayName: 'ReportsMenu', route: "reports" }, render: function () { - return React.DOM.div(null, "Reports Menu"); + return React.createElement("div", null, "Reports Menu"); } }); @@ -695,15 +701,16 @@ var header_entries = [MainMenu, ToolsMenu, ReportsMenu]; var Header = React.createClass({displayName: 'Header', + mixins: [ReactRouter.Navigation], getInitialState: function () { return { active: header_entries[0] }; }, - handleClick: function (active) { - ReactRouter.transitionTo(active.route); + handleClick: function (active, e) { + e.preventDefault(); + this.transitionTo(active.route); this.setState({active: active}); - return false; }, handleFileClick: function () { console.log("File click"); @@ -714,7 +721,7 @@ var Header = React.createClass({displayName: 'Header', active: entry == this.state.active }); return ( - React.DOM.a({key: i, + React.createElement("a", {key: i, href: "#", className: classes, onClick: this.handleClick.bind(this, entry) @@ -725,29 +732,26 @@ var Header = React.createClass({displayName: 'Header', }.bind(this)); return ( - React.DOM.header(null, - React.DOM.div({className: "title-bar"}, + React.createElement("header", null, + React.createElement("div", {className: "title-bar"}, "mitmproxy ", this.props.settings.version ), - React.DOM.nav({className: "nav-tabs nav-tabs-lg"}, - React.DOM.a({href: "#", className: "special", onClick: this.handleFileClick}, " File "), + React.createElement("nav", {className: "nav-tabs nav-tabs-lg"}, + React.createElement("a", {href: "#", className: "special", onClick: this.handleFileClick}, " File "), header ), - React.DOM.div({className: "menu"}, - this.state.active({settings: this.props.settings}) + React.createElement("div", {className: "menu"}, + React.createElement(this.state.active, {settings: this.props.settings}) ) ) ); } }); -/** @jsx React.DOM */ - - var TLSColumn = React.createClass({displayName: 'TLSColumn', statics: { renderTitle: function(){ - return React.DOM.th({key: "tls", className: "col-tls"}); + return React.createElement("th", {key: "tls", className: "col-tls"}); } }, render: function(){ @@ -759,7 +763,7 @@ var TLSColumn = React.createClass({displayName: 'TLSColumn', } else { classes = "col-tls col-tls-http"; } - return React.DOM.td({className: classes}); + return React.createElement("td", {className: classes}); } }); @@ -767,7 +771,7 @@ var TLSColumn = React.createClass({displayName: 'TLSColumn', var IconColumn = React.createClass({displayName: 'IconColumn', statics: { renderTitle: function(){ - return React.DOM.th({key: "icon", className: "col-icon"}); + return React.createElement("th", {key: "icon", className: "col-icon"}); } }, render: function(){ @@ -798,19 +802,19 @@ var IconColumn = React.createClass({displayName: 'IconColumn', icon += " resource-icon"; - return React.DOM.td({className: "col-icon"}, React.DOM.div({className: icon})); + return React.createElement("td", {className: "col-icon"}, React.createElement("div", {className: icon})); } }); var PathColumn = React.createClass({displayName: 'PathColumn', statics: { renderTitle: function(){ - return React.DOM.th({key: "path", className: "col-path"}, "Path"); + return React.createElement("th", {key: "path", className: "col-path"}, "Path"); } }, render: function(){ var flow = this.props.flow; - return React.DOM.td({className: "col-path"}, flow.request.scheme + "://" + flow.request.host + flow.request.path); + return React.createElement("td", {className: "col-path"}, flow.request.scheme + "://" + flow.request.host + flow.request.path); } }); @@ -818,12 +822,12 @@ var PathColumn = React.createClass({displayName: 'PathColumn', var MethodColumn = React.createClass({displayName: 'MethodColumn', statics: { renderTitle: function(){ - return React.DOM.th({key: "method", className: "col-method"}, "Method"); + return React.createElement("th", {key: "method", className: "col-method"}, "Method"); } }, render: function(){ var flow = this.props.flow; - return React.DOM.td({className: "col-method"}, flow.request.method); + return React.createElement("td", {className: "col-method"}, flow.request.method); } }); @@ -831,7 +835,7 @@ var MethodColumn = React.createClass({displayName: 'MethodColumn', var StatusColumn = React.createClass({displayName: 'StatusColumn', statics: { renderTitle: function(){ - return React.DOM.th({key: "status", className: "col-status"}, "Status"); + return React.createElement("th", {key: "status", className: "col-status"}, "Status"); } }, render: function(){ @@ -842,7 +846,7 @@ var StatusColumn = React.createClass({displayName: 'StatusColumn', } else { status = null; } - return React.DOM.td({className: "col-status"}, status); + return React.createElement("td", {className: "col-status"}, status); } }); @@ -850,7 +854,7 @@ var StatusColumn = React.createClass({displayName: 'StatusColumn', var SizeColumn = React.createClass({displayName: 'SizeColumn', statics: { renderTitle: function(){ - return React.DOM.th({key: "size", className: "col-size"}, "Size"); + return React.createElement("th", {key: "size", className: "col-size"}, "Size"); } }, render: function(){ @@ -861,7 +865,7 @@ var SizeColumn = React.createClass({displayName: 'SizeColumn', total += flow.response.contentLength || 0; } var size = formatSize(total); - return React.DOM.td({className: "col-size"}, size); + return React.createElement("td", {className: "col-size"}, size); } }); @@ -869,7 +873,7 @@ var SizeColumn = React.createClass({displayName: 'SizeColumn', var TimeColumn = React.createClass({displayName: 'TimeColumn', statics: { renderTitle: function(){ - return React.DOM.th({key: "time", className: "col-time"}, "Time"); + return React.createElement("th", {key: "time", className: "col-time"}, "Time"); } }, render: function(){ @@ -880,7 +884,7 @@ var TimeColumn = React.createClass({displayName: 'TimeColumn', } else { time = "..."; } - return React.DOM.td({className: "col-time"}, time); + return React.createElement("td", {className: "col-time"}, time); } }); @@ -895,20 +899,18 @@ var all_columns = [ 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}); + var columns = this.props.columns.map(function(Column){ + return React.createElement(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)}, + React.createElement("tr", {className: className, onClick: this.props.selectFlow.bind(null, flow)}, columns )); }, @@ -926,7 +928,7 @@ var FlowTableHead = React.createClass({displayName: 'FlowTableHead', var columns = this.props.columns.map(function(column){ return column.renderTitle(); }.bind(this)); - return React.DOM.thead(null, React.DOM.tr(null, columns)); + return React.createElement("thead", null, React.createElement("tr", null, columns)); } }); @@ -934,7 +936,7 @@ 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, + return React.createElement(FlowRow, {key: flow.id, ref: flow.id, flow: flow, columns: this.props.columns, @@ -942,7 +944,7 @@ var FlowTableBody = React.createClass({displayName: 'FlowTableBody', selectFlow: this.props.selectFlow} ); }.bind(this)); - return React.DOM.tbody(null, rows); + return React.createElement("tbody", null, rows); } }); @@ -975,11 +977,11 @@ var FlowTable = React.createClass({displayName: 'FlowTable', }, render: function () { return ( - React.DOM.div({className: "flow-table", onScroll: this.adjustHead}, - React.DOM.table(null, - FlowTableHead({ref: "head", + React.createElement("div", {className: "flow-table", onScroll: this.adjustHead}, + React.createElement("table", null, + React.createElement(FlowTableHead, {ref: "head", columns: this.state.columns}), - FlowTableBody({ref: "body", + React.createElement(FlowTableBody, {ref: "body", flows: this.props.flows, selected: this.props.selected, selectFlow: this.props.selectFlow, @@ -990,44 +992,42 @@ var FlowTable = React.createClass({displayName: 'FlowTable', } }); -/** @jsx React.DOM */ - var FlowDetailNav = React.createClass({displayName: 'FlowDetailNav', - render: function(){ + render: function () { - var items = this.props.tabs.map(function(e){ + var items = this.props.tabs.map(function (e) { var str = e.charAt(0).toUpperCase() + e.slice(1); var className = this.props.active === e ? "active" : ""; - var onClick = function(){ + var onClick = function (e) { this.props.selectTab(e); - return false; + e.preventDefault(); }.bind(this); - return React.DOM.a({key: e, - href: "#", - className: className, - onClick: onClick}, str); + return React.createElement("a", {key: e, + href: "#", + className: className, + onClick: onClick}, str); }.bind(this)); return ( - React.DOM.nav({ref: "head", className: "nav-tabs nav-tabs-sm"}, + React.createElement("nav", {ref: "head", className: "nav-tabs nav-tabs-sm"}, items ) ); - } + } }); var Headers = React.createClass({displayName: 'Headers', - render: function(){ - var rows = this.props.message.headers.map(function(header, i){ + render: function () { + var rows = this.props.message.headers.map(function (header, i) { return ( - React.DOM.tr({key: i}, - React.DOM.td({className: "header-name"}, header[0]+":"), - React.DOM.td({className: "header-value"}, header[1]) + React.createElement("tr", {key: i}, + React.createElement("td", {className: "header-name"}, header[0] + ":"), + React.createElement("td", {className: "header-value"}, header[1]) ) ); }); return ( - React.DOM.table({className: "header-table"}, - React.DOM.tbody(null, + React.createElement("table", {className: "header-table"}, + React.createElement("tbody", null, rows ) ) @@ -1036,27 +1036,27 @@ var Headers = React.createClass({displayName: 'Headers', }); var FlowDetailRequest = React.createClass({displayName: 'FlowDetailRequest', - render: function(){ + render: function () { var flow = this.props.flow; var first_line = [ - flow.request.method, - RequestUtils.pretty_url(flow.request), - "HTTP/"+ flow.response.httpversion.join(".") - ].join(" "); + flow.request.method, + RequestUtils.pretty_url(flow.request), + "HTTP/" + flow.response.httpversion.join(".") + ].join(" "); var content = null; - if(flow.request.contentLength > 0){ - content = "Request Content Size: "+ formatSize(flow.request.contentLength); + if (flow.request.contentLength > 0) { + content = "Request Content Size: " + formatSize(flow.request.contentLength); } else { - content = React.DOM.div({className: "alert alert-info"}, "No Content"); + content = React.createElement("div", {className: "alert alert-info"}, "No Content"); } //TODO: Styling return ( - React.DOM.section(null, - React.DOM.div({className: "first-line"}, first_line ), - Headers({message: flow.request}), - React.DOM.hr(null), + React.createElement("section", null, + React.createElement("div", {className: "first-line"}, first_line ), + React.createElement(Headers, {message: flow.request}), + React.createElement("hr", null), content ) ); @@ -1064,27 +1064,27 @@ var FlowDetailRequest = React.createClass({displayName: 'FlowDetailRequest', }); var FlowDetailResponse = React.createClass({displayName: 'FlowDetailResponse', - render: function(){ + render: function () { var flow = this.props.flow; var first_line = [ - "HTTP/"+ flow.response.httpversion.join("."), - flow.response.code, - flow.response.msg - ].join(" "); + "HTTP/" + flow.response.httpversion.join("."), + flow.response.code, + flow.response.msg + ].join(" "); var content = null; - if(flow.response.contentLength > 0){ - content = "Response Content Size: "+ formatSize(flow.response.contentLength); + if (flow.response.contentLength > 0) { + content = "Response Content Size: " + formatSize(flow.response.contentLength); } else { - content = React.DOM.div({className: "alert alert-info"}, "No Content"); + content = React.createElement("div", {className: "alert alert-info"}, "No Content"); } //TODO: Styling return ( - React.DOM.section(null, - React.DOM.div({className: "first-line"}, first_line ), - Headers({message: flow.response}), - React.DOM.hr(null), + React.createElement("section", null, + React.createElement("div", {className: "first-line"}, first_line ), + React.createElement(Headers, {message: flow.response}), + React.createElement("hr", null), content ) ); @@ -1092,42 +1092,53 @@ var FlowDetailResponse = React.createClass({displayName: 'FlowDetailResponse', }); var TimeStamp = React.createClass({displayName: 'TimeStamp', - render: function() { + render: function () { - if(!this.props.t){ + if (!this.props.t) { //should be return null, but that triggers a React bug. - return React.DOM.tr(null); + return React.createElement("tr", null); } var ts = (new Date(this.props.t * 1000)).toISOString(); - ts = ts.replace("T", " ").replace("Z",""); + ts = ts.replace("T", " ").replace("Z", ""); var delta; - if(this.props.deltaTo){ - delta = formatTimeDelta(1000 * (this.props.t-this.props.deltaTo)); - delta = React.DOM.span({className: "text-muted"}, "(" + delta + ")"); + if (this.props.deltaTo) { + delta = formatTimeDelta(1000 * (this.props.t - this.props.deltaTo)); + delta = React.createElement("span", {className: "text-muted"}, "(" + delta + ")"); } else { delta = null; } - return React.DOM.tr(null, React.DOM.td(null, this.props.title + ":"), React.DOM.td(null, ts, " ", delta)); + return React.createElement("tr", null, + React.createElement("td", null, this.props.title + ":"), + React.createElement("td", null, ts, " ", delta) + ); } }); var ConnectionInfo = React.createClass({displayName: 'ConnectionInfo', - render: function() { + render: function () { var conn = this.props.conn; var address = conn.address.address.join(":"); - var sni = React.DOM.tr({key: "sni"}); //should be null, but that triggers a React bug. - if(conn.sni){ - sni = React.DOM.tr({key: "sni"}, React.DOM.td(null, React.DOM.abbr({title: "TLS Server Name Indication"}, "TLS SNI:")), React.DOM.td(null, conn.sni)); + var sni = React.createElement("tr", {key: "sni"}); //should be null, but that triggers a React bug. + if (conn.sni) { + sni = React.createElement("tr", {key: "sni"}, + React.createElement("td", null, + React.createElement("abbr", {title: "TLS Server Name Indication"}, "TLS SNI:") + ), + React.createElement("td", null, conn.sni) + ); } return ( - React.DOM.table({className: "connection-table"}, - React.DOM.tbody(null, - React.DOM.tr({key: "address"}, React.DOM.td(null, "Address:"), React.DOM.td(null, address)), + React.createElement("table", {className: "connection-table"}, + React.createElement("tbody", null, + React.createElement("tr", {key: "address"}, + React.createElement("td", null, "Address:"), + React.createElement("td", null, address) + ), sni ) ) @@ -1136,7 +1147,7 @@ var ConnectionInfo = React.createClass({displayName: 'ConnectionInfo', }); var CertificateInfo = React.createClass({displayName: 'CertificateInfo', - render: function(){ + render: function () { //TODO: We should fetch human-readable certificate representation // from the server var flow = this.props.flow; @@ -1145,19 +1156,19 @@ var CertificateInfo = React.createClass({displayName: 'CertificateInfo', var preStyle = {maxHeight: 100}; return ( - React.DOM.div(null, - client_conn.cert ? React.DOM.h4(null, "Client Certificate") : null, - client_conn.cert ? React.DOM.pre({style: preStyle}, client_conn.cert) : null, + React.createElement("div", null, + client_conn.cert ? React.createElement("h4", null, "Client Certificate") : null, + client_conn.cert ? React.createElement("pre", {style: preStyle}, client_conn.cert) : null, - server_conn.cert ? React.DOM.h4(null, "Server Certificate") : null, - server_conn.cert ? React.DOM.pre({style: preStyle}, server_conn.cert) : null + server_conn.cert ? React.createElement("h4", null, "Server Certificate") : null, + server_conn.cert ? React.createElement("pre", {style: preStyle}, server_conn.cert) : null ) ); } }); var Timing = React.createClass({displayName: 'Timing', - render: function(){ + render: function () { var flow = this.props.flow; var sc = flow.server_conn; var cc = flow.client_conn; @@ -1210,46 +1221,46 @@ var Timing = React.createClass({displayName: 'Timing', } //Add unique key for each row. - timestamps.forEach(function(e){ + timestamps.forEach(function (e) { e.key = e.title; }); timestamps = _.sortBy(timestamps, 't'); - var rows = timestamps.map(function(e){ - return TimeStamp(e); + var rows = timestamps.map(function (e) { + return React.createElement(TimeStamp, React.__spread({}, e)); }); return ( - React.DOM.div(null, - React.DOM.h4(null, "Timing"), - React.DOM.table({className: "timing-table"}, - React.DOM.tbody(null, + React.createElement("div", null, + React.createElement("h4", null, "Timing"), + React.createElement("table", {className: "timing-table"}, + React.createElement("tbody", null, rows + ) ) ) - ) ); } }); var FlowDetailConnectionInfo = React.createClass({displayName: 'FlowDetailConnectionInfo', - render: function(){ + render: function () { var flow = this.props.flow; var client_conn = flow.client_conn; var server_conn = flow.server_conn; return ( - React.DOM.section(null, + React.createElement("section", null, - React.DOM.h4(null, "Client Connection"), - ConnectionInfo({conn: client_conn}), + React.createElement("h4", null, "Client Connection"), + React.createElement(ConnectionInfo, {conn: client_conn}), - React.DOM.h4(null, "Server Connection"), - ConnectionInfo({conn: server_conn}), + React.createElement("h4", null, "Server Connection"), + React.createElement(ConnectionInfo, {conn: server_conn}), - CertificateInfo({flow: flow}), + React.createElement(CertificateInfo, {flow: flow}), - Timing({flow: flow}) + React.createElement(Timing, {flow: flow}) ) ); @@ -1263,100 +1274,120 @@ var tabs = { }; var FlowDetail = React.createClass({displayName: 'FlowDetail', - getDefaultProps: function(){ + getDefaultProps: function () { return { - tabs: ["request","response", "details"] + tabs: ["request", "response", "details"] }; }, - mixins: [StickyHeadMixin], - nextTab: function(i) { + mixins: [StickyHeadMixin, ReactRouter.Navigation, ReactRouter.State], + nextTab: function (i) { var currentIndex = this.props.tabs.indexOf(this.props.active); // JS modulo operator doesn't correct negative numbers, make sure that we are positive. var nextIndex = (currentIndex + i + this.props.tabs.length) % this.props.tabs.length; - this.props.selectTab(this.props.tabs[nextIndex]); + this.selectTab(this.props.tabs[nextIndex]); }, - render: function(){ + selectTab: function (panel) { + this.replaceWith( + "flow", + { + flowId: this.getParams().flowId, + detailTab: panel + } + ); + }, + 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({ref: "head", - tabs: this.props.tabs, - active: this.props.active, - selectTab: this.props.selectTab}), - Tab({flow: this.props.flow}) + React.createElement("div", {className: "flow-detail", onScroll: this.adjustHead}, + React.createElement(FlowDetailNav, {ref: "head", + tabs: this.props.tabs, + active: this.props.active, + selectTab: this.selectTab}), + React.createElement(Tab, {flow: this.props.flow}) ) - ); - } + ); + } }); -/** @jsx React.DOM */ - var MainView = React.createClass({displayName: 'MainView', - getInitialState: function() { + mixins: [ReactRouter.Navigation, ReactRouter.State], + getInitialState: function () { return { - flows: [], + flows: [] }; }, + componentWillReceiveProps: function (nextProps) { + if (nextProps.flowStore !== this.props.flowStore) { + this.closeView(); + this.openView(nextProps.flowStore); + } + }, + openView: function (store) { + var view = new FlowView(store); + this.setState({ + view: view + }); + view.addListener("add", this.onFlowChange); + view.addListener("update", this.onFlowChange); + view.addListener("remove", this.onFlowChange); + view.addListener("recalculate", this.onFlowChange); + }, + closeView: function () { + this.state.view.close(); + }, componentDidMount: function () { - //FIXME: The store should be global, move out of here. - window.flowstore = new LiveFlowStore(); - - this.flowStore = window.flowstore.open_view(); - this.flowStore.addListener("add",this.onFlowChange); - this.flowStore.addListener("update",this.onFlowChange); - this.flowStore.addListener("remove",this.onFlowChange); - this.flowStore.addListener("recalculate",this.onFlowChange); + this.openView(this.props.flowStore); }, componentWillUnmount: function () { - this.flowStore.removeListener("change",this.onFlowChange); - this.flowStore.close(); + this.closeView(); }, onFlowChange: function () { + console.warn("onFlowChange is deprecated"); this.setState({ - flows: this.flowStore.flows + flows: this.state.view.flows }); }, - selectDetailTab: function(panel) { - ReactRouter.replaceWith( - "flow", - { - flowId: this.props.params.flowId, - detailTab: panel - } - ); - }, - selectFlow: function(flow) { - if(flow){ - ReactRouter.replaceWith( - "flow", + selectFlow: function (flow) { + if (flow) { + this.replaceWith( + "flow", { flowId: flow.id, - detailTab: this.props.params.detailTab || "request" + detailTab: this.getParams().detailTab || "request" } ); - this.refs.flowTable.scrollIntoView(flow); + console.log("TODO: Scroll into view"); + //this.refs.flowTable.scrollIntoView(flow); } else { - ReactRouter.replaceWith("flows"); + this.replaceWith("flows"); } }, - selectFlowRelative: function(i){ + selectFlowRelative: function (i) { + var flows = this.state.view.flows; var index; - if(!this.props.params.flowId){ - if(i > 0){ - index = this.state.flows.length-1; + if (!this.getParams().flowId) { + if (i > 0) { + index = flows.length - 1; } else { index = 0; } } else { - index = _.findIndex(this.state.flows, function(f){ - return f.id === this.props.params.flowId; - }.bind(this)); - index = Math.min(Math.max(0, index+i), this.state.flows.length-1); + i = flows.length; + var currFlowId = this.getParams().flowId; + while (i--) { + if (flows[i].id === currFlowId) { + index = i; + break; + } + } + index = Math.min( + Math.max(0, index + i), + flows.length - 1); } - this.selectFlow(this.state.flows[index]); + this.selectFlow(flows[index]); }, - onKeyDown: function(e){ - switch(e.keyCode){ + onKeyDown: function (e) { + switch (e.keyCode) { case Key.K: case Key.UP: this.selectFlowRelative(-1); @@ -1377,14 +1408,14 @@ var MainView = React.createClass({displayName: 'MainView', break; case Key.H: case Key.LEFT: - if(this.refs.flowDetails){ + if (this.refs.flowDetails) { this.refs.flowDetails.nextTab(-1); } break; case Key.L: case Key.TAB: case Key.RIGHT: - if(this.refs.flowDetails){ + if (this.refs.flowDetails) { this.refs.flowDetails.nextTab(+1); } break; @@ -1392,53 +1423,50 @@ var MainView = React.createClass({displayName: 'MainView', console.debug("keydown", e.keyCode); return; } - return false; + e.preventDefault(); }, - render: function() { - var selected = _.find(this.state.flows, { id: this.props.params.flowId }); + render: function () { + var selected = this.props.flowStore.get(this.getParams().flowId); var details; - if(selected){ + if (selected) { details = ( - FlowDetail({ref: "flowDetails", - flow: selected, - selectTab: this.selectDetailTab, - active: this.props.params.detailTab}) + React.createElement(FlowDetail, {ref: "flowDetails", + flow: selected, + active: this.getParams().detailTab}) ); } else { details = null; } return ( - React.DOM.div({className: "main-view", onKeyDown: this.onKeyDown, tabIndex: "0"}, - FlowTable({ref: "flowTable", - flows: this.state.flows, - selectFlow: this.selectFlow, - selected: selected}), - details ? Splitter(null) : null, + React.createElement("div", {className: "main-view", onKeyDown: this.onKeyDown, tabIndex: "0"}, + React.createElement(FlowTable, {ref: "flowTable", + flows: this.state.view ? this.state.view.flows : [], + selectFlow: this.selectFlow, + selected: selected}), + details ? React.createElement(Splitter, null) : null, details ) ); } }); -/** @jsx React.DOM */ - var LogMessage = React.createClass({displayName: 'LogMessage', render: function(){ var entry = this.props.entry; var indicator; switch(entry.level){ case "web": - indicator = React.DOM.i({className: "fa fa-fw fa-html5"}); + indicator = React.createElement("i", {className: "fa fa-fw fa-html5"}); break; case "debug": - indicator = React.DOM.i({className: "fa fa-fw fa-bug"}); + indicator = React.createElement("i", {className: "fa fa-fw fa-bug"}); break; default: - indicator = React.DOM.i({className: "fa fa-fw fa-info"}); + indicator = React.createElement("i", {className: "fa fa-fw fa-info"}); } return ( - React.DOM.div(null, + React.createElement("div", null, indicator, " ", entry.message ) ); @@ -1473,14 +1501,15 @@ var EventLogContents = React.createClass({displayName: 'EventLogContents', if(!this.props.filter[row.level]){ return null; } - return LogMessage({key: row.id, entry: row}); + return React.createElement(LogMessage, {key: row.id, entry: row}); }.bind(this)); - return React.DOM.pre(null, messages); + return React.createElement("pre", null, messages); } }); var ToggleFilter = React.createClass({displayName: 'ToggleFilter', - toggle: function(){ + toggle: function(e){ + e.preventDefault(); return this.props.toggleLevel(this.props.name); }, render: function(){ @@ -1491,7 +1520,7 @@ var ToggleFilter = React.createClass({displayName: 'ToggleFilter', className += "label-default"; } return ( - React.DOM.a({ + React.createElement("a", { href: "#", className: className, onClick: this.toggle}, @@ -1520,52 +1549,50 @@ var EventLog = React.createClass({displayName: 'EventLog', var filter = this.state.filter; filter[level] = !filter[level]; this.setState({filter: filter}); - return false; }, render: function () { return ( - React.DOM.div({className: "eventlog"}, - React.DOM.div(null, + React.createElement("div", {className: "eventlog"}, + React.createElement("div", null, "Eventlog", - React.DOM.div({className: "pull-right"}, - ToggleFilter({name: "debug", active: this.state.filter.debug, toggleLevel: this.toggleLevel}), - ToggleFilter({name: "info", active: this.state.filter.info, toggleLevel: this.toggleLevel}), - ToggleFilter({name: "web", active: this.state.filter.web, toggleLevel: this.toggleLevel}), - React.DOM.i({onClick: this.close, className: "fa fa-close"}) + React.createElement("div", {className: "pull-right"}, + React.createElement(ToggleFilter, {name: "debug", active: this.state.filter.debug, toggleLevel: this.toggleLevel}), + React.createElement(ToggleFilter, {name: "info", active: this.state.filter.info, toggleLevel: this.toggleLevel}), + React.createElement(ToggleFilter, {name: "web", active: this.state.filter.web, toggleLevel: this.toggleLevel}), + React.createElement("i", {onClick: this.close, className: "fa fa-close"}) ) ), - EventLogContents({filter: this.state.filter}) + React.createElement(EventLogContents, {filter: this.state.filter}) ) ); } }); -/** @jsx React.DOM */ - var Footer = React.createClass({displayName: 'Footer', render: function () { var mode = this.props.settings.mode; return ( - React.DOM.footer(null, - mode != "regular" ? React.DOM.span({className: "label label-success"}, mode, " mode") : null + React.createElement("footer", null, + mode != "regular" ? React.createElement("span", {className: "label label-success"}, mode, " mode") : null ) ); } }); -/** @jsx React.DOM */ - //TODO: Move out of here, just a stub. var Reports = React.createClass({displayName: 'Reports', render: function () { - return React.DOM.div(null, "ReportEditor"); + return React.createElement("div", null, "ReportEditor"); } }); var ProxyAppMain = React.createClass({displayName: 'ProxyAppMain', getInitialState: function () { - return { settings: SettingsStore.getAll() }; + return { + settings: SettingsStore.getAll(), + flowStore: new LiveFlowStore() + }; }, componentDidMount: function () { SettingsStore.addListener("change", this.onSettingsChange); @@ -1578,37 +1605,37 @@ var ProxyAppMain = React.createClass({displayName: 'ProxyAppMain', }, render: function () { return ( - React.DOM.div({id: "container"}, - Header({settings: this.state.settings}), - this.props.activeRouteHandler({settings: this.state.settings}), - this.state.settings.showEventLog ? Splitter({axis: "y"}) : null, - this.state.settings.showEventLog ? EventLog(null) : null, - Footer({settings: this.state.settings}) + React.createElement("div", {id: "container"}, + React.createElement(Header, {settings: this.state.settings}), + React.createElement(RouteHandler, {settings: this.state.settings, flowStore: this.state.flowStore}), + this.state.settings.showEventLog ? React.createElement(Splitter, {axis: "y"}) : null, + this.state.settings.showEventLog ? React.createElement(EventLog, null) : null, + React.createElement(Footer, {settings: this.state.settings}) ) - ); + ); } }); -var Routes = ReactRouter.Routes; var Route = ReactRouter.Route; +var RouteHandler = ReactRouter.RouteHandler; var Redirect = ReactRouter.Redirect; var DefaultRoute = ReactRouter.DefaultRoute; var NotFoundRoute = ReactRouter.NotFoundRoute; -var ProxyApp = ( - 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}), - Redirect({path: "/", to: "flows"}) - ) +var routes = ( + React.createElement(Route, {path: "/", handler: ProxyAppMain}, + React.createElement(Route, {name: "flows", path: "flows", handler: MainView}), + React.createElement(Route, {name: "flow", path: "flows/:flowId/:detailTab", handler: MainView}), + React.createElement(Route, {name: "reports", handler: Reports}), + React.createElement(Redirect, {path: "/", to: "flows"}) ) - ); +); $(function () { - window.app = React.renderComponent(ProxyApp, document.body); + ReactRouter.run(routes, function (Handler) { + React.render(React.createElement(Handler, null), document.body); + }); var UpdateConnection = new Connection("/updates"); UpdateConnection.onmessage = function (message) { var m = JSON.parse(message.data); -- cgit v1.2.3 From e41c0be293502619b6a072fb9bcd8bbd8694b86a Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 27 Nov 2014 01:40:26 +0100 Subject: format code --- libmproxy/web/static/js/app.js | 301 +++++++++++++++++++++-------------------- 1 file changed, 153 insertions(+), 148 deletions(-) (limited to 'libmproxy/web/static/js/app.js') diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index d8d908e8..9be33253 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -41,8 +41,8 @@ var Key = { var formatSize = function (bytes) { var size = bytes; var prefix = ["B", "KB", "MB", "GB", "TB"]; - var i=0; - while (Math.abs(size) >= 1024 && i < prefix.length-1) { + var i = 0; + while (Math.abs(size) >= 1024 && i < prefix.length - 1) { i++; size = size / 1024; } @@ -80,7 +80,7 @@ Dispatcher.prototype.unregister = function (callback) { }; Dispatcher.prototype.dispatch = function (payload) { console.debug("dispatch", payload); - for(var i = 0; i < this.callbacks.length; i++){ + for (var i = 0; i < this.callbacks.length; i++) { this.callbacks[i](payload); } }; @@ -123,13 +123,13 @@ var SettingsActions = { var event_id = 0; var EventLogActions = { - add_event: function(message){ + add_event: function (message) { AppDispatcher.dispatchViewAction({ type: ActionTypes.ADD_EVENT, data: { message: message, level: "web", - id: "viewAction-"+event_id++ + id: "viewAction-" + event_id++ } }); } @@ -264,7 +264,7 @@ _.extend(EventLogView.prototype, EventEmitter.prototype, { }, add: function (entry) { this.log.push(entry); - if(this.log.length > 200){ + if (this.log.length > 200) { this.log.shift(); } this.emit("change"); @@ -289,37 +289,37 @@ _.extend(_EventLogStore.prototype, EventEmitter.prototype, { var view = new EventLogView(this, !since); return view; /* - //TODO: Really do bulk retrieval of last messages. - window.setTimeout(function () { - view.add_bulk([ - { - id: 1, - message: "Hello World" - }, - { - id: 2, - message: "I was already transmitted as an event." - } - ]); - }, 100); - - var id = 2; - view.add({ - id: id++, - message: "I was already transmitted as an event." - }); - view.add({ - id: id++, - message: "I was only transmitted as an event before the bulk was added.." - }); - window.setInterval(function () { - view.add({ - id: id++, - message: "." - }); - }, 1000); - return view; - */ + //TODO: Really do bulk retrieval of last messages. + window.setTimeout(function () { + view.add_bulk([ + { + id: 1, + message: "Hello World" + }, + { + id: 2, + message: "I was already transmitted as an event." + } + ]); + }, 100); + + var id = 2; + view.add({ + id: id++, + message: "I was already transmitted as an event." + }); + view.add({ + id: id++, + message: "I was only transmitted as an event before the bulk was added.." + }); + window.setInterval(function () { + view.add({ + id: id++, + message: "." + }); + }, 1000); + return view; + */ }, handle: function (action) { switch (action.type) { @@ -374,7 +374,7 @@ _.extend(FlowStore.prototype, { this._pos_map[flow.id] = i; } }, - get: function(flow_id){ + get: function (flow_id) { return this._flow_list[this._pos_map[flow_id]]; } }); @@ -392,12 +392,12 @@ function LiveFlowStore(endpoint) { }.bind(this); } _.extend(LiveFlowStore.prototype, FlowStore.prototype, { - close: function(){ + close: function () { this.conn.close(); }, - add: function(flow) { + add: function (flow) { // Make sure that deferred adds don't add an element twice. - if(!this._pos_map[flow.id]){ + if (!this._pos_map[flow.id]) { FlowStore.prototype.add.call(this, flow); } }, @@ -454,7 +454,7 @@ function FlowView(store, filt, sort) { } _.extend(FlowView.prototype, EventEmitter.prototype, { - close: function(){ + close: function () { this.store._views = _.without(this.store._views, this); }, recalculate: function (flows, filt, sort) { @@ -518,22 +518,22 @@ _.extend(FlowView.prototype, EventEmitter.prototype, { } }); function Connection(url) { - if(url[0] != "/"){ + if (url[0] != "/") { this.url = url; } else { this.url = location.origin.replace("http", "ws") + url; } var ws = new WebSocket(this.url); - ws.onopen = function(){ + ws.onopen = function () { this.onopen.apply(this, arguments); }.bind(this); - ws.onmessage = function(){ + ws.onmessage = function () { this.onmessage.apply(this, arguments); }.bind(this); - ws.onerror = function(){ + ws.onerror = function () { this.onerror.apply(this, arguments); }.bind(this); - ws.onclose = function(){ + ws.onclose = function () { this.onclose.apply(this, arguments); }.bind(this); this.ws = ws; @@ -552,7 +552,7 @@ Connection.prototype.onclose = function (close) { EventLogActions.add_event("WebSocket Connection closed."); console.debug("onclose", this, arguments); }; -Connection.prototype.close = function(){ +Connection.prototype.close = function () { this.ws.close(); }; //React utils. For other utilities, see ../utils.js @@ -563,85 +563,85 @@ var Splitter = React.createClass({displayName: 'Splitter', axis: "x" }; }, - getInitialState: function(){ + getInitialState: function () { return { applied: false, startX: false, startY: false }; }, - onMouseDown: function(e){ + onMouseDown: function (e) { this.setState({ startX: e.pageX, startY: e.pageY }); - window.addEventListener("mousemove",this.onMouseMove); - window.addEventListener("mouseup",this.onMouseUp); + window.addEventListener("mousemove", this.onMouseMove); + window.addEventListener("mouseup", this.onMouseUp); // Occasionally, only a dragEnd event is triggered, but no mouseUp. - window.addEventListener("dragend",this.onDragEnd); + window.addEventListener("dragend", this.onDragEnd); }, - onDragEnd: function(){ - this.getDOMNode().style.transform=""; - window.removeEventListener("dragend",this.onDragEnd); - window.removeEventListener("mouseup",this.onMouseUp); - window.removeEventListener("mousemove",this.onMouseMove); + onDragEnd: function () { + this.getDOMNode().style.transform = ""; + window.removeEventListener("dragend", this.onDragEnd); + window.removeEventListener("mouseup", this.onMouseUp); + window.removeEventListener("mousemove", this.onMouseMove); }, - onMouseUp: function(e){ + onMouseUp: function (e) { this.onDragEnd(); var node = this.getDOMNode(); var prev = node.previousElementSibling; var next = node.nextElementSibling; - var dX = e.pageX-this.state.startX; - var dY = e.pageY-this.state.startY; + var dX = e.pageX - this.state.startX; + var dY = e.pageY - this.state.startY; var flexBasis; - if(this.props.axis === "x"){ + if (this.props.axis === "x") { flexBasis = prev.offsetWidth + dX; } else { flexBasis = prev.offsetHeight + dY; } - prev.style.flex = "0 0 "+Math.max(0, flexBasis)+"px"; + prev.style.flex = "0 0 " + Math.max(0, flexBasis) + "px"; next.style.flex = "1 1 auto"; this.setState({ applied: true }); }, - onMouseMove: function(e){ + onMouseMove: function (e) { var dX = 0, dY = 0; - if(this.props.axis === "x"){ - dX = e.pageX-this.state.startX; + if (this.props.axis === "x") { + dX = e.pageX - this.state.startX; } else { - dY = e.pageY-this.state.startY; + dY = e.pageY - this.state.startY; } - this.getDOMNode().style.transform = "translate("+dX+"px,"+dY+"px)"; + this.getDOMNode().style.transform = "translate(" + dX + "px," + dY + "px)"; }, - reset: function(willUnmount) { + reset: function (willUnmount) { if (!this.state.applied) { return; } var node = this.getDOMNode(); var prev = node.previousElementSibling; var next = node.nextElementSibling; - + prev.style.flex = ""; next.style.flex = ""; - if(!willUnmount){ + if (!willUnmount) { this.setState({ applied: false }); } }, - componentWillUnmount: function(){ + componentWillUnmount: function () { this.reset(true); }, - render: function(){ + render: function () { var className = "splitter"; - if(this.props.axis === "x"){ + if (this.props.axis === "x") { className += " splitter-x"; } else { className += " splitter-y"; @@ -667,10 +667,11 @@ var MainMenu = React.createClass({displayName: 'MainMenu', return ( React.createElement("div", null, React.createElement("button", {className: "btn " + (this.props.settings.showEventLog ? "btn-primary" : "btn-default"), onClick: this.toggleEventLog}, - React.createElement("i", {className: "fa fa-database"}), " Display Event Log" + React.createElement("i", {className: "fa fa-database"}), + "Display Event Log" ) ) - ); + ); } }); @@ -716,25 +717,25 @@ var Header = React.createClass({displayName: 'Header', console.log("File click"); }, render: function () { - var header = header_entries.map(function(entry, i){ + var header = header_entries.map(function (entry, i) { var classes = React.addons.classSet({ active: entry == this.state.active }); return ( React.createElement("a", {key: i, - href: "#", - className: classes, - onClick: this.handleClick.bind(this, entry) + href: "#", + className: classes, + onClick: this.handleClick.bind(this, entry) }, entry.title ) - ); + ); }.bind(this)); - + return ( React.createElement("header", null, React.createElement("div", {className: "title-bar"}, - "mitmproxy ", this.props.settings.version + "mitmproxy ", this.props.settings.version ), React.createElement("nav", {className: "nav-tabs nav-tabs-lg"}, React.createElement("a", {href: "#", className: "special", onClick: this.handleFileClick}, " File "), @@ -744,21 +745,21 @@ var Header = React.createClass({displayName: 'Header', React.createElement(this.state.active, {settings: this.props.settings}) ) ) - ); + ); } }); var TLSColumn = React.createClass({displayName: 'TLSColumn', statics: { - renderTitle: function(){ + renderTitle: function () { return React.createElement("th", {key: "tls", className: "col-tls"}); } }, - render: function(){ + render: function () { var flow = this.props.flow; var ssl = (flow.request.scheme == "https"); var classes; - if(ssl){ + if (ssl) { classes = "col-tls col-tls-https"; } else { classes = "col-tls col-tls-http"; @@ -770,23 +771,23 @@ var TLSColumn = React.createClass({displayName: 'TLSColumn', var IconColumn = React.createClass({displayName: 'IconColumn', statics: { - renderTitle: function(){ + renderTitle: function () { return React.createElement("th", {key: "icon", className: "col-icon"}); } }, - render: function(){ + render: function () { var flow = this.props.flow; var icon; - if(flow.response){ + if (flow.response) { var contentType = ResponseUtils.getContentType(flow.response); //TODO: We should assign a type to the flow somewhere else. - if(flow.response.code == 304) { + if (flow.response.code == 304) { icon = "resource-icon-not-modified"; - } else if(300 <= flow.response.code && flow.response.code < 400) { + } else if (300 <= flow.response.code && flow.response.code < 400) { icon = "resource-icon-redirect"; - } else if(contentType && contentType.indexOf("image") >= 0) { + } else if (contentType && contentType.indexOf("image") >= 0) { icon = "resource-icon-image"; } else if (contentType && contentType.indexOf("javascript") >= 0) { icon = "resource-icon-js"; @@ -796,23 +797,25 @@ var IconColumn = React.createClass({displayName: 'IconColumn', icon = "resource-icon-document"; } } - if(!icon){ + if (!icon) { icon = "resource-icon-plain"; } icon += " resource-icon"; - return React.createElement("td", {className: "col-icon"}, React.createElement("div", {className: icon})); + return React.createElement("td", {className: "col-icon"}, + React.createElement("div", {className: icon}) + ); } }); var PathColumn = React.createClass({displayName: 'PathColumn', statics: { - renderTitle: function(){ + renderTitle: function () { return React.createElement("th", {key: "path", className: "col-path"}, "Path"); } }, - render: function(){ + render: function () { var flow = this.props.flow; return React.createElement("td", {className: "col-path"}, flow.request.scheme + "://" + flow.request.host + flow.request.path); } @@ -821,11 +824,11 @@ var PathColumn = React.createClass({displayName: 'PathColumn', var MethodColumn = React.createClass({displayName: 'MethodColumn', statics: { - renderTitle: function(){ + renderTitle: function () { return React.createElement("th", {key: "method", className: "col-method"}, "Method"); } }, - render: function(){ + render: function () { var flow = this.props.flow; return React.createElement("td", {className: "col-method"}, flow.request.method); } @@ -834,14 +837,14 @@ var MethodColumn = React.createClass({displayName: 'MethodColumn', var StatusColumn = React.createClass({displayName: 'StatusColumn', statics: { - renderTitle: function(){ + renderTitle: function () { return React.createElement("th", {key: "status", className: "col-status"}, "Status"); } }, - render: function(){ + render: function () { var flow = this.props.flow; var status; - if(flow.response){ + if (flow.response) { status = flow.response.code; } else { status = null; @@ -853,15 +856,15 @@ var StatusColumn = React.createClass({displayName: 'StatusColumn', var SizeColumn = React.createClass({displayName: 'SizeColumn', statics: { - renderTitle: function(){ + renderTitle: function () { return React.createElement("th", {key: "size", className: "col-size"}, "Size"); } }, - render: function(){ + render: function () { var flow = this.props.flow; var total = flow.request.contentLength; - if(flow.response){ + if (flow.response) { total += flow.response.contentLength || 0; } var size = formatSize(total); @@ -872,14 +875,14 @@ var SizeColumn = React.createClass({displayName: 'SizeColumn', var TimeColumn = React.createClass({displayName: 'TimeColumn', statics: { - renderTitle: function(){ + renderTitle: function () { return React.createElement("th", {key: "time", className: "col-time"}, "Time"); } }, - render: function(){ + render: function () { var flow = this.props.flow; var time; - if(flow.response){ + if (flow.response) { time = formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start)); } else { time = "..."; @@ -900,13 +903,13 @@ var all_columns = [ var FlowRow = React.createClass({displayName: 'FlowRow', - render: function(){ + render: function () { var flow = this.props.flow; - var columns = this.props.columns.map(function(Column){ + var columns = this.props.columns.map(function (Column) { return React.createElement(Column, {key: Column.displayName, flow: flow}); }.bind(this)); var className = ""; - if(this.props.selected){ + if (this.props.selected) { className += "selected"; } return ( @@ -914,35 +917,37 @@ var FlowRow = React.createClass({displayName: 'FlowRow', columns )); }, - shouldComponentUpdate: function(nextProps){ + shouldComponentUpdate: function (nextProps) { var isEqual = ( - this.props.columns.length === nextProps.columns.length && - this.props.selected === nextProps.selected && - this.props.flow.response === nextProps.flow.response); + this.props.columns.length === nextProps.columns.length && + this.props.selected === nextProps.selected && + this.props.flow.response === nextProps.flow.response); return !isEqual; } }); var FlowTableHead = React.createClass({displayName: 'FlowTableHead', - render: function(){ - var columns = this.props.columns.map(function(column){ + render: function () { + var columns = this.props.columns.map(function (column) { return column.renderTitle(); }.bind(this)); - return React.createElement("thead", null, React.createElement("tr", null, columns)); + return React.createElement("thead", null, + React.createElement("tr", null, columns) + ); } }); var FlowTableBody = React.createClass({displayName: 'FlowTableBody', - render: function(){ - var rows = this.props.flows.map(function(flow){ + render: function () { + var rows = this.props.flows.map(function (flow) { var selected = (flow == this.props.selected); return React.createElement(FlowRow, {key: flow.id, - ref: flow.id, - flow: flow, - columns: this.props.columns, - selected: selected, - selectFlow: this.props.selectFlow} - ); + ref: flow.id, + flow: flow, + columns: this.props.columns, + selected: selected, + selectFlow: this.props.selectFlow} + ); }.bind(this)); return React.createElement("tbody", null, rows); } @@ -956,7 +961,7 @@ var FlowTable = React.createClass({displayName: 'FlowTable', columns: all_columns }; }, - scrollIntoView: function(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(); @@ -969,9 +974,9 @@ var FlowTable = React.createClass({displayName: 'FlowTable', // -thead_height pixel earlier. flowNode_top -= this.refs.body.getDOMNode().offsetTop; - if(flowNode_top < viewport_top){ + if (flowNode_top < viewport_top) { viewport.scrollTop = flowNode_top; - } else if(flowNode_bottom > viewport_bottom) { + } else if (flowNode_bottom > viewport_bottom) { viewport.scrollTop = flowNode_bottom - viewport.offsetHeight; } }, @@ -980,15 +985,15 @@ var FlowTable = React.createClass({displayName: 'FlowTable', React.createElement("div", {className: "flow-table", onScroll: this.adjustHead}, React.createElement("table", null, React.createElement(FlowTableHead, {ref: "head", - columns: this.state.columns}), + columns: this.state.columns}), React.createElement(FlowTableBody, {ref: "body", - flows: this.props.flows, - selected: this.props.selected, - selectFlow: this.props.selectFlow, - columns: this.state.columns}) + flows: this.props.flows, + selected: this.props.selected, + selectFlow: this.props.selectFlow, + columns: this.state.columns}) ) ) - ); + ); } }); @@ -1452,10 +1457,10 @@ var MainView = React.createClass({displayName: 'MainView', } }); var LogMessage = React.createClass({displayName: 'LogMessage', - render: function(){ + render: function () { var entry = this.props.entry; var indicator; - switch(entry.level){ + switch (entry.level) { case "web": indicator = React.createElement("i", {className: "fa fa-fw fa-html5"}); break; @@ -1471,13 +1476,13 @@ var LogMessage = React.createClass({displayName: 'LogMessage', ) ); }, - shouldComponentUpdate: function(){ + shouldComponentUpdate: function () { return false; // log entries are immutable. } }); var EventLogContents = React.createClass({displayName: 'EventLogContents', - mixins:[AutoScrollMixin], + mixins: [AutoScrollMixin], getInitialState: function () { return { log: [] @@ -1497,8 +1502,8 @@ var EventLogContents = React.createClass({displayName: 'EventLogContents', }); }, render: function () { - var messages = this.state.log.map(function(row) { - if(!this.props.filter[row.level]){ + var messages = this.state.log.map(function (row) { + if (!this.props.filter[row.level]) { return null; } return React.createElement(LogMessage, {key: row.id, entry: row}); @@ -1508,11 +1513,11 @@ var EventLogContents = React.createClass({displayName: 'EventLogContents', }); var ToggleFilter = React.createClass({displayName: 'ToggleFilter', - toggle: function(e){ + toggle: function (e) { e.preventDefault(); return this.props.toggleLevel(this.props.name); }, - render: function(){ + render: function () { var className = "label "; if (this.props.active) { className += "label-primary"; @@ -1527,11 +1532,11 @@ var ToggleFilter = React.createClass({displayName: 'ToggleFilter', this.props.name ) ); - } + } }); var EventLog = React.createClass({displayName: 'EventLog', - getInitialState: function(){ + getInitialState: function () { return { filter: { "debug": false, @@ -1545,7 +1550,7 @@ var EventLog = React.createClass({displayName: 'EventLog', showEventLog: false }); }, - toggleLevel: function(level){ + toggleLevel: function (level) { var filter = this.state.filter; filter[level] = !filter[level]; this.setState({filter: filter}); @@ -1554,7 +1559,7 @@ var EventLog = React.createClass({displayName: 'EventLog', return ( React.createElement("div", {className: "eventlog"}, React.createElement("div", null, - "Eventlog", + "Eventlog", React.createElement("div", {className: "pull-right"}, React.createElement(ToggleFilter, {name: "debug", active: this.state.filter.debug, toggleLevel: this.toggleLevel}), React.createElement(ToggleFilter, {name: "info", active: this.state.filter.info, toggleLevel: this.toggleLevel}), @@ -1575,7 +1580,7 @@ var Footer = React.createClass({displayName: 'Footer', React.createElement("footer", null, mode != "regular" ? React.createElement("span", {className: "label label-success"}, mode, " mode") : null ) - ); + ); } }); -- cgit v1.2.3 From 9eecc8d6e237022b328f6eaee48b64287336b258 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 27 Nov 2014 02:34:03 +0100 Subject: web: fixes --- libmproxy/web/static/js/app.js | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) (limited to 'libmproxy/web/static/js/app.js') diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index 9be33253..78108b38 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -441,38 +441,46 @@ SortByInsertionOrder.prototype.key = function (flow) { var default_sort = (new SortByInsertionOrder()).key; -function FlowView(store, filt, sort) { +function FlowView(store, filt, sortfun) { EventEmitter.call(this); filt = filt || function (flow) { return true; }; - sort = sort || default_sort; + sortfun = sortfun || default_sort; this.store = store; this.store._views.push(this); - this.recalculate(this.store._flow_list, filt, sort); + this.recalculate(this.store._flow_list, filt, sortfun); } _.extend(FlowView.prototype, EventEmitter.prototype, { close: function () { this.store._views = _.without(this.store._views, this); }, - recalculate: function (flows, filt, sort) { + recalculate: function (flows, filt, sortfun) { if (filt) { this.filt = filt; } - if (sort) { - this.sort = sort; + if (sortfun) { + this.sortfun = sortfun; } + + //Ugly workaround: Call .sortfun() for each flow once in order, + //so that SortByInsertionOrder make sense. + var i = flows.length; + while(i--){ + this.sortfun(flows[i]); + } + this.flows = flows.filter(this.filt); this.flows.sort(function (a, b) { - return this.sort(a) - this.sort(b); + return this.sortfun(b) - this.sortfun(a); }.bind(this)); this.emit("recalculate"); }, add: function (flow) { if (this.filt(flow)) { - var idx = _.sortedIndex(this.flows, flow, this.sort); + var idx = _.sortedIndex(this.flows, flow, this.sortfun); if (idx === this.flows.length) { //happens often, .push is way faster. this.flows.push(flow); } else { @@ -497,7 +505,7 @@ _.extend(FlowView.prototype, EventEmitter.prototype, { } else if (!this.filt(flow)) { this.remove(flow.id); } else { - if (this.sort(this.flows[idx]) !== this.sort(flow)) { //sortpos has changed + if (this.sortfun(this.flows[idx]) !== this.sortfun(flow)) { //sortpos has changed this.remove(this.flows[idx]); this.add(flow); } else { @@ -1367,18 +1375,18 @@ var MainView = React.createClass({displayName: 'MainView', this.replaceWith("flows"); } }, - selectFlowRelative: function (i) { + selectFlowRelative: function (shift) { var flows = this.state.view.flows; var index; if (!this.getParams().flowId) { - if (i > 0) { + if (shift > 0) { index = flows.length - 1; } else { index = 0; } } else { - i = flows.length; var currFlowId = this.getParams().flowId; + var i = flows.length; while (i--) { if (flows[i].id === currFlowId) { index = i; @@ -1386,7 +1394,7 @@ var MainView = React.createClass({displayName: 'MainView', } } index = Math.min( - Math.max(0, index + i), + Math.max(0, index + shift), flows.length - 1); } this.selectFlow(flows[index]); -- cgit v1.2.3 From 7ca1ac0f3b7856c0ae44bfbf3b27ae4a424a1cc2 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 28 Nov 2014 16:03:56 +0100 Subject: web: virtual scrolling --- libmproxy/web/static/js/app.js | 143 +++++++++++++++++++++++++++-------------- 1 file changed, 95 insertions(+), 48 deletions(-) (limited to 'libmproxy/web/static/js/app.js') diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index 78108b38..dd1259f4 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -193,18 +193,22 @@ EventEmitter.prototype.emit = function (event) { listener.apply(this, args); }.bind(this)); }; -EventEmitter.prototype.addListener = function (event, f) { - this.listeners[event] = this.listeners[event] || []; - this.listeners[event].push(f); +EventEmitter.prototype.addListener = function (events, f) { + events.split(" ").forEach(function (event) { + this.listeners[event] = this.listeners[event] || []; + this.listeners[event].push(f); + }.bind(this)); }; -EventEmitter.prototype.removeListener = function (event, f) { - if (!(event in this.listeners)) { +EventEmitter.prototype.removeListener = function (events, f) { + if (!(events in this.listeners)) { return false; } - var index = this.listeners[event].indexOf(f); - if (index >= 0) { - this.listeners[event].splice(index, 1); - } + events.split(" ").forEach(function (event) { + var index = this.listeners[event].indexOf(f); + if (index >= 0) { + this.listeners[event].splice(index, 1); + } + }.bind(this)); }; function _SettingsStore() { @@ -926,11 +930,13 @@ var FlowRow = React.createClass({displayName: 'FlowRow', )); }, shouldComponentUpdate: function (nextProps) { - var isEqual = ( - this.props.columns.length === nextProps.columns.length && - this.props.selected === nextProps.selected && - this.props.flow.response === nextProps.flow.response); - return !isEqual; + return true; + // Further optimization could be done here + // by calling forceUpdate on flow updates, selection changes and column changes. + //return ( + //(this.props.columns.length !== nextProps.columns.length) || + //(this.props.selected !== nextProps.selected) + //); } }); @@ -945,30 +951,51 @@ var FlowTableHead = React.createClass({displayName: 'FlowTableHead', } }); -var FlowTableBody = React.createClass({displayName: 'FlowTableBody', - render: function () { - var rows = this.props.flows.map(function (flow) { - var selected = (flow == this.props.selected); - return React.createElement(FlowRow, {key: flow.id, - ref: flow.id, - flow: flow, - columns: this.props.columns, - selected: selected, - selectFlow: this.props.selectFlow} - ); - }.bind(this)); - return React.createElement("tbody", null, rows); - } -}); +var ROW_HEIGHT = 32; var FlowTable = React.createClass({displayName: 'FlowTable', mixins: [StickyHeadMixin, AutoScrollMixin], getInitialState: function () { return { - columns: all_columns + columns: all_columns, + start: 0, + stop: 0 }; }, + componentWillMount: function () { + if (this.props.view) { + this.props.view.addListener("add update remove recalculate", this.onChange); + } + }, + componentWillReceiveProps: function (nextProps) { + if (nextProps.view !== this.props.view) { + if (this.props.view) { + this.props.view.removeListener("add update remove recalculate"); + } + nextProps.view.addListener("add update remove recalculate", this.onChange); + } + }, + componentDidMount: function () { + this.onScroll(); + }, + onScroll: function () { + this.adjustHead(); + + var viewport = this.getDOMNode(); + var top = viewport.scrollTop; + var height = viewport.offsetHeight; + var start = Math.floor(top / ROW_HEIGHT); + var stop = start + Math.ceil(height / ROW_HEIGHT); + this.setState({ + start: start, + stop: stop + }); + }, + onChange: function () { + console.log("onChange"); + this.forceUpdate(); + }, scrollIntoView: function (flow) { // Now comes the fun part: Scroll the flow into the view. var viewport = this.getDOMNode(); @@ -989,16 +1016,46 @@ var FlowTable = React.createClass({displayName: 'FlowTable', } }, render: function () { + var space_top = 0, space_bottom = 0, fix_nth_row = null; + var rows = []; + if (this.props.view) { + var flows = this.props.view.flows; + var max = Math.min(flows.length, this.state.stop); + console.log("render", this.props.view.flows.length, this.state.start, max - this.state.start, flows.length - this.state.stop); + + for (var i = this.state.start; i < max; i++) { + var flow = flows[i]; + var selected = (flow === this.props.selected); + rows.push( + React.createElement(FlowRow, {key: flow.id, + ref: flow.id, + flow: flow, + columns: this.state.columns, + selected: selected, + selectFlow: this.props.selectFlow} + ) + ); + } + + space_top = this.state.start * ROW_HEIGHT; + space_bottom = Math.max(0, flows.length - this.state.stop) * ROW_HEIGHT; + if(this.state.start % 2 === 1){ + fix_nth_row = React.createElement("tr", null); + } + } + + return ( - React.createElement("div", {className: "flow-table", onScroll: this.adjustHead}, + React.createElement("div", {className: "flow-table", onScroll: this.onScroll}, React.createElement("table", null, React.createElement(FlowTableHead, {ref: "head", columns: this.state.columns}), - React.createElement(FlowTableBody, {ref: "body", - flows: this.props.flows, - selected: this.props.selected, - selectFlow: this.props.selectFlow, - columns: this.state.columns}) + React.createElement("tbody", null, + React.createElement("tr", {style: {height: space_top}}), + fix_nth_row, + rows, + React.createElement("tr", {style: {height: space_bottom}}) + ) ) ) ); @@ -1340,26 +1397,16 @@ var MainView = React.createClass({displayName: 'MainView', this.setState({ view: view }); - view.addListener("add", this.onFlowChange); - view.addListener("update", this.onFlowChange); - view.addListener("remove", this.onFlowChange); - view.addListener("recalculate", this.onFlowChange); }, closeView: function () { this.state.view.close(); }, - componentDidMount: function () { + componentWillMount: function () { this.openView(this.props.flowStore); }, componentWillUnmount: function () { this.closeView(); }, - onFlowChange: function () { - console.warn("onFlowChange is deprecated"); - this.setState({ - flows: this.state.view.flows - }); - }, selectFlow: function (flow) { if (flow) { this.replaceWith( @@ -1455,7 +1502,7 @@ var MainView = React.createClass({displayName: 'MainView', return ( React.createElement("div", {className: "main-view", onKeyDown: this.onKeyDown, tabIndex: "0"}, React.createElement(FlowTable, {ref: "flowTable", - flows: this.state.view ? this.state.view.flows : [], + view: this.state.view, selectFlow: this.selectFlow, selected: selected}), details ? React.createElement(Splitter, null) : null, -- cgit v1.2.3 From c39b6e4277357c9da1dfd5e3e8c41b5b3427e0ce Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 28 Nov 2014 19:16:47 +0100 Subject: web: various fixes, add clear button --- libmproxy/web/static/js/app.js | 105 ++++++++++++++++++++++++++++------------- 1 file changed, 72 insertions(+), 33 deletions(-) (limited to 'libmproxy/web/static/js/app.js') diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index dd1259f4..64d6ba44 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -386,7 +386,8 @@ _.extend(FlowStore.prototype, { function LiveFlowStore(endpoint) { FlowStore.call(this); - this.updates_before_init = []; // (empty array is true in js) + this.updates_before_fetch = undefined; + this._fetchxhr = false; this.endpoint = endpoint || "/flows"; this.conn = new Connection(this.endpoint + "/updates"); this.conn.onopen = this._onopen.bind(this); @@ -401,33 +402,46 @@ _.extend(LiveFlowStore.prototype, FlowStore.prototype, { }, add: function (flow) { // Make sure that deferred adds don't add an element twice. - if (!this._pos_map[flow.id]) { + if (!(flow.id in this._pos_map)) { FlowStore.prototype.add.call(this, flow); } }, + _onopen: function () { + //Update stream openend, fetch list of flows. + console.log("Update Connection opened, fetching flows..."); + this.fetch(); + }, + fetch: function () { + if (this._fetchxhr) { + this._fetchxhr.abort(); + } + this._fetchxhr = $.getJSON(this.endpoint, this.handle_fetch.bind(this)); + this.updates_before_fetch = []; // (JS: empty array is true) + }, handle_update: function (type, data) { console.log("LiveFlowStore.handle_update", type, data); - if (this.updates_before_init) { + + if (type === "reset") { + return this.fetch(); + } + + if (this.updates_before_fetch) { console.log("defer update", type, data); - this.updates_before_init.push(arguments); + this.updates_before_fetch.push(arguments); } else { this[type](data); } }, handle_fetch: function (data) { + this._fetchxhr = false; console.log("Flows fetched."); this.reset(data.flows); - var updates = this.updates_before_init; - this.updates_before_init = false; + var updates = this.updates_before_fetch; + this.updates_before_fetch = false; for (var i = 0; i < updates.length; i++) { this.handle_update.apply(this, updates[i]); } }, - _onopen: function () { - //Update stream openend, fetch list of flows. - console.log("Update Connection opened, fetching flows..."); - $.getJSON(this.endpoint, this.handle_fetch.bind(this)); - }, }); function SortByInsertionOrder() { @@ -471,20 +485,22 @@ _.extend(FlowView.prototype, EventEmitter.prototype, { //Ugly workaround: Call .sortfun() for each flow once in order, //so that SortByInsertionOrder make sense. - var i = flows.length; - while(i--){ + for(var i = 0; i < flows.length; i++) { this.sortfun(flows[i]); } this.flows = flows.filter(this.filt); this.flows.sort(function (a, b) { - return this.sortfun(b) - this.sortfun(a); + return this.sortfun(a) - this.sortfun(b); }.bind(this)); this.emit("recalculate"); }, + index: function (flow) { + return _.sortedIndex(this.flows, flow, this.sortfun); + }, add: function (flow) { if (this.filt(flow)) { - var idx = _.sortedIndex(this.flows, flow, this.sortfun); + var idx = this.index(flow); if (idx === this.flows.length) { //happens often, .push is way faster. this.flows.push(flow); } else { @@ -665,6 +681,23 @@ var Splitter = React.createClass({displayName: 'Splitter', ); } }); + +function getCookie(name) { + var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); + return r ? r[1] : undefined; +} +var xsrf = $.param({_xsrf: getCookie("_xsrf")}); + +//Tornado XSRF Protection. +$.ajaxPrefilter(function(options){ + if(options.type === "post" && options.url[0] === "/"){ + if(options.data){ + options.data += ("&" + xsrf); + } else { + options.data = xsrf; + } + } +}); var MainMenu = React.createClass({displayName: 'MainMenu', statics: { title: "Traffic", @@ -675,12 +708,17 @@ var MainMenu = React.createClass({displayName: 'MainMenu', showEventLog: !this.props.settings.showEventLog }); }, + clearFlows: function(){ + $.post("/flows/clear"); + }, render: function () { return ( React.createElement("div", null, React.createElement("button", {className: "btn " + (this.props.settings.showEventLog ? "btn-primary" : "btn-default"), onClick: this.toggleEventLog}, - React.createElement("i", {className: "fa fa-database"}), - "Display Event Log" + React.createElement("i", {className: "fa fa-database"}), " Display Event Log" + ), " ", + React.createElement("button", {className: "btn btn-default", onClick: this.clearFlows}, + React.createElement("i", {className: "fa fa-eraser"}), " Clear Flows" ) ) ); @@ -999,20 +1037,23 @@ var FlowTable = React.createClass({displayName: 'FlowTable', 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(); + var thead_height = this.refs.body.getDOMNode().offsetTop; + + var flow_top = (this.props.view.index(flow) * ROW_HEIGHT) + thead_height; + var viewport_top = viewport.scrollTop; var viewport_bottom = viewport_top + viewport.offsetHeight; - var flowNode_top = flowNode.offsetTop; - var flowNode_bottom = flowNode_top + flowNode.offsetHeight; + var flow_bottom = flow_top + ROW_HEIGHT; - // Account for pinned thead by pretending that the flowNode starts - // -thead_height pixel earlier. - flowNode_top -= this.refs.body.getDOMNode().offsetTop; + // Account for pinned thead - if (flowNode_top < viewport_top) { - viewport.scrollTop = flowNode_top; - } else if (flowNode_bottom > viewport_bottom) { - viewport.scrollTop = flowNode_bottom - viewport.offsetHeight; + + console.log("scrollInto", flow_top, flow_bottom, viewport_top, viewport_bottom, thead_height); + + if (flow_top - thead_height < viewport_top) { + viewport.scrollTop = flow_top - thead_height; + } else if (flow_bottom > viewport_bottom) { + viewport.scrollTop = flow_bottom - viewport.offsetHeight; } }, render: function () { @@ -1050,7 +1091,7 @@ var FlowTable = React.createClass({displayName: 'FlowTable', React.createElement("table", null, React.createElement(FlowTableHead, {ref: "head", columns: this.state.columns}), - React.createElement("tbody", null, + React.createElement("tbody", {ref: "body"}, React.createElement("tr", {style: {height: space_top}}), fix_nth_row, rows, @@ -1068,9 +1109,9 @@ var FlowDetailNav = React.createClass({displayName: 'FlowDetailNav', var items = this.props.tabs.map(function (e) { var str = e.charAt(0).toUpperCase() + e.slice(1); var className = this.props.active === e ? "active" : ""; - var onClick = function (e) { + var onClick = function (event) { this.props.selectTab(e); - e.preventDefault(); + event.preventDefault(); }.bind(this); return React.createElement("a", {key: e, href: "#", @@ -1366,7 +1407,6 @@ var FlowDetail = React.createClass({displayName: 'FlowDetail', ); }, render: function () { - var flow = JSON.stringify(this.props.flow, null, 2); var Tab = tabs[this.props.active]; return ( React.createElement("div", {className: "flow-detail", onScroll: this.adjustHead}, @@ -1416,8 +1456,7 @@ var MainView = React.createClass({displayName: 'MainView', detailTab: this.getParams().detailTab || "request" } ); - console.log("TODO: Scroll into view"); - //this.refs.flowTable.scrollIntoView(flow); + this.refs.flowTable.scrollIntoView(flow); } else { this.replaceWith("flows"); } -- cgit v1.2.3 From dd1a45140c8a6cb3f6c5d7247120c05fa37cdf24 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 28 Nov 2014 20:03:04 +0100 Subject: web: add virtualscroll mixin --- libmproxy/web/static/js/app.js | 119 ++++++++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 48 deletions(-) (limited to 'libmproxy/web/static/js/app.js') diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index 64d6ba44..09fb71e6 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -698,6 +698,60 @@ $.ajaxPrefilter(function(options){ } } }); +var VirtualScrollMixin = { + getInitialState: function () { + return { + start: 0, + stop: 0 + } + }, + getPlaceholderTop: function () { + var style = { + height: this.state.start * this.props.rowHeight + }; + var spacer = React.createElement("tr", {key: "placeholder-top", style: style}); + + if (this.state.start % 2 === 1) { + // fix even/odd rows + return [spacer, React.createElement("tr", {key: "placeholder-top-2"})]; + } else { + return spacer; + } + }, + getPlaceholderBottom: function (total) { + var style = { + height: Math.max(0, total - this.state.stop) * this.props.rowHeight + }; + return React.createElement("tr", {key: "placeholder-bottom", style: style}); + }, + onScroll: function () { + var viewport = this.getDOMNode(); + var top = viewport.scrollTop; + var height = viewport.offsetHeight; + var start = Math.floor(top / this.props.rowHeight); + var stop = start + Math.ceil(height / this.props.rowHeight); + this.setState({ + start: start, + stop: stop + }); + }, + scrollRowIntoView: function(index, head_height){ + + var row_top = (index * this.props.rowHeight) + head_height; + var row_bottom = row_top + this.props.rowHeight; + + var viewport = this.getDOMNode(); + var viewport_top = viewport.scrollTop; + var viewport_bottom = viewport_top + viewport.offsetHeight; + + // Account for pinned thead + if (row_top - head_height < viewport_top) { + viewport.scrollTop = row_top - head_height; + } else if (row_bottom > viewport_bottom) { + viewport.scrollTop = row_bottom - viewport.offsetHeight; + } + }, +}; var MainMenu = React.createClass({displayName: 'MainMenu', statics: { title: "Traffic", @@ -993,12 +1047,10 @@ var FlowTableHead = React.createClass({displayName: 'FlowTableHead', var ROW_HEIGHT = 32; var FlowTable = React.createClass({displayName: 'FlowTable', - mixins: [StickyHeadMixin, AutoScrollMixin], + mixins: [StickyHeadMixin, AutoScrollMixin, VirtualScrollMixin], getInitialState: function () { return { - columns: all_columns, - start: 0, - stop: 0 + columns: all_columns }; }, componentWillMount: function () { @@ -1015,46 +1067,26 @@ var FlowTable = React.createClass({displayName: 'FlowTable', } }, componentDidMount: function () { - this.onScroll(); + this.onScroll2(); }, - onScroll: function () { + getDefaultProps: function () { + return { + rowHeight: ROW_HEIGHT + }; + }, + onScroll2: function () { this.adjustHead(); - - var viewport = this.getDOMNode(); - var top = viewport.scrollTop; - var height = viewport.offsetHeight; - var start = Math.floor(top / ROW_HEIGHT); - var stop = start + Math.ceil(height / ROW_HEIGHT); - this.setState({ - start: start, - stop: stop - }); + this.onScroll(); }, onChange: function () { console.log("onChange"); this.forceUpdate(); }, scrollIntoView: function (flow) { - // Now comes the fun part: Scroll the flow into the view. - var viewport = this.getDOMNode(); - var thead_height = this.refs.body.getDOMNode().offsetTop; - - var flow_top = (this.props.view.index(flow) * ROW_HEIGHT) + thead_height; - - var viewport_top = viewport.scrollTop; - var viewport_bottom = viewport_top + viewport.offsetHeight; - var flow_bottom = flow_top + ROW_HEIGHT; - - // Account for pinned thead - - - console.log("scrollInto", flow_top, flow_bottom, viewport_top, viewport_bottom, thead_height); - - if (flow_top - thead_height < viewport_top) { - viewport.scrollTop = flow_top - thead_height; - } else if (flow_bottom > viewport_bottom) { - viewport.scrollTop = flow_bottom - viewport.offsetHeight; - } + this.scrollRowIntoView( + this.props.view.index(flow), + this.refs.body.getDOMNode().offsetTop + ); }, render: function () { var space_top = 0, space_bottom = 0, fix_nth_row = null; @@ -1062,7 +1094,6 @@ var FlowTable = React.createClass({displayName: 'FlowTable', if (this.props.view) { var flows = this.props.view.flows; var max = Math.min(flows.length, this.state.stop); - console.log("render", this.props.view.flows.length, this.state.start, max - this.state.start, flows.length - this.state.stop); for (var i = this.state.start; i < max; i++) { var flow = flows[i]; @@ -1077,25 +1108,17 @@ var FlowTable = React.createClass({displayName: 'FlowTable', ) ); } - - space_top = this.state.start * ROW_HEIGHT; - space_bottom = Math.max(0, flows.length - this.state.stop) * ROW_HEIGHT; - if(this.state.start % 2 === 1){ - fix_nth_row = React.createElement("tr", null); - } } - return ( - React.createElement("div", {className: "flow-table", onScroll: this.onScroll}, + React.createElement("div", {className: "flow-table", onScroll: this.onScroll2}, React.createElement("table", null, React.createElement(FlowTableHead, {ref: "head", columns: this.state.columns}), React.createElement("tbody", {ref: "body"}, - React.createElement("tr", {style: {height: space_top}}), - fix_nth_row, + this.getPlaceholderTop(), rows, - React.createElement("tr", {style: {height: space_bottom}}) + this.getPlaceholderBottom(flows.length) ) ) ) -- cgit v1.2.3 From f6c0e000da504a68ecd41a8f7ce59e2f71e0a218 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 28 Nov 2014 20:54:52 +0100 Subject: event log: virtual scrolling --- libmproxy/web/static/js/app.js | 107 ++++++++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 38 deletions(-) (limited to 'libmproxy/web/static/js/app.js') diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index 09fb71e6..f8749c5e 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -485,7 +485,7 @@ _.extend(FlowView.prototype, EventEmitter.prototype, { //Ugly workaround: Call .sortfun() for each flow once in order, //so that SortByInsertionOrder make sense. - for(var i = 0; i < flows.length; i++) { + for (var i = 0; i < flows.length; i++) { this.sortfun(flows[i]); } @@ -705,35 +705,57 @@ var VirtualScrollMixin = { stop: 0 } }, + componentWillMount: function(){ + if(!this.props.rowHeight){ + console.warn("VirtualScrollMixin: No rowHeight specified", this); + } + }, getPlaceholderTop: function () { + var Tag = this.props.placeholderTagName || "tr"; var style = { height: this.state.start * this.props.rowHeight }; - var spacer = React.createElement("tr", {key: "placeholder-top", style: style}); + var spacer = React.createElement(Tag, {key: "placeholder-top", style: style}); if (this.state.start % 2 === 1) { // fix even/odd rows - return [spacer, React.createElement("tr", {key: "placeholder-top-2"})]; + return [spacer, React.createElement(Tag, {key: "placeholder-top-2"})]; } else { return spacer; } }, getPlaceholderBottom: function (total) { + var Tag = this.props.placeholderTagName || "tr"; var style = { height: Math.max(0, total - this.state.stop) * this.props.rowHeight }; - return React.createElement("tr", {key: "placeholder-bottom", style: style}); + return React.createElement(Tag, {key: "placeholder-bottom", style: style}); + }, + componentDidMount: function () { + this.onScroll(); }, onScroll: function () { var viewport = this.getDOMNode(); var top = viewport.scrollTop; var height = viewport.offsetHeight; var start = Math.floor(top / this.props.rowHeight); - var stop = start + Math.ceil(height / this.props.rowHeight); + var stop = start + Math.ceil(height / (this.props.rowHeightMin || this.props.rowHeight)); + this.setState({ start: start, stop: stop }); + console.log(start, stop); + }, + renderRows: function(elems){ + var rows = []; + var max = Math.min(elems.length, this.state.stop); + + for (var i = this.state.start; i < max; i++) { + var elem = elems[i]; + rows.push(this.renderRow(elem)); + } + return rows; }, scrollRowIntoView: function(index, head_height){ @@ -1066,9 +1088,6 @@ var FlowTable = React.createClass({displayName: 'FlowTable', nextProps.view.addListener("add update remove recalculate", this.onChange); } }, - componentDidMount: function () { - this.onScroll2(); - }, getDefaultProps: function () { return { rowHeight: ROW_HEIGHT @@ -1088,27 +1107,20 @@ var FlowTable = React.createClass({displayName: 'FlowTable', this.refs.body.getDOMNode().offsetTop ); }, + renderRow: function (flow) { + var selected = (flow === this.props.selected); + return React.createElement(FlowRow, {key: flow.id, + ref: flow.id, + flow: flow, + columns: this.state.columns, + selected: selected, + selectFlow: this.props.selectFlow} + ); + }, render: function () { - var space_top = 0, space_bottom = 0, fix_nth_row = null; - var rows = []; - if (this.props.view) { - var flows = this.props.view.flows; - var max = Math.min(flows.length, this.state.stop); - - for (var i = this.state.start; i < max; i++) { - var flow = flows[i]; - var selected = (flow === this.props.selected); - rows.push( - React.createElement(FlowRow, {key: flow.id, - ref: flow.id, - flow: flow, - columns: this.state.columns, - selected: selected, - selectFlow: this.props.selectFlow} - ) - ); - } - } + var flows = this.props.view ? this.props.view.flows : []; + + var rows = this.renderRows(flows); return ( React.createElement("div", {className: "flow-table", onScroll: this.onScroll2}, @@ -1599,7 +1611,7 @@ var LogMessage = React.createClass({displayName: 'LogMessage', }); var EventLogContents = React.createClass({displayName: 'EventLogContents', - mixins: [AutoScrollMixin], + mixins: [AutoScrollMixin, VirtualScrollMixin], getInitialState: function () { return { log: [] @@ -1614,18 +1626,37 @@ var EventLogContents = React.createClass({displayName: 'EventLogContents', this.log.close(); }, onEventLogChange: function () { + var log = this.log.getAll().filter(function (entry) { + return this.props.filter[entry.level]; + }.bind(this)); this.setState({ - log: this.log.getAll() + log: log }); }, + componentWillReceiveProps: function () { + if (this.log) { + this.onEventLogChange(); + } + + }, + getDefaultProps: function () { + return { + rowHeight: 45, + rowHeightMin: 15, + placeholderTagName: "div" + }; + }, + renderRow: function(elem){ + return React.createElement(LogMessage, {key: elem.id, entry: elem}); + }, render: function () { - var messages = this.state.log.map(function (row) { - if (!this.props.filter[row.level]) { - return null; - } - return React.createElement(LogMessage, {key: row.id, entry: row}); - }.bind(this)); - return React.createElement("pre", null, messages); + var rows = this.renderRows(this.state.log); + + return React.createElement("pre", {onScroll: this.onScroll}, + this.getPlaceholderTop(), + rows, + this.getPlaceholderBottom(this.state.log.length) + ); } }); @@ -1676,7 +1707,7 @@ var EventLog = React.createClass({displayName: 'EventLog', return ( React.createElement("div", {className: "eventlog"}, React.createElement("div", null, - "Eventlog", + "Eventlog", React.createElement("div", {className: "pull-right"}, React.createElement(ToggleFilter, {name: "debug", active: this.state.filter.debug, toggleLevel: this.toggleLevel}), React.createElement(ToggleFilter, {name: "info", active: this.state.filter.info, toggleLevel: this.toggleLevel}), -- cgit v1.2.3 From 096a3af273ccb309820351b466e62382f62a2c36 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sat, 29 Nov 2014 03:25:07 +0100 Subject: web: various improvements --- libmproxy/web/static/js/app.js | 113 ++++++++++++++++++++++++++++++----------- 1 file changed, 84 insertions(+), 29 deletions(-) (limited to 'libmproxy/web/static/js/app.js') diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index f8749c5e..f2b8fe4a 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -60,6 +60,11 @@ var formatTimeDelta = function (milliseconds) { } return Math.round(time) + prefix[i]; }; + +var formatTimeStamp = function (seconds) { + var ts = (new Date(seconds * 1000)).toISOString(); + return ts.replace("T", " ").replace("Z", ""); +}; const PayloadSources = { VIEW: "view", SERVER: "server" @@ -434,7 +439,7 @@ _.extend(LiveFlowStore.prototype, FlowStore.prototype, { }, handle_fetch: function (data) { this._fetchxhr = false; - console.log("Flows fetched."); + console.log("Flows fetched.", this.updates_before_fetch); this.reset(data.flows); var updates = this.updates_before_fetch; this.updates_before_fetch = false; @@ -703,7 +708,7 @@ var VirtualScrollMixin = { return { start: 0, stop: 0 - } + }; }, componentWillMount: function(){ if(!this.props.rowHeight){ @@ -745,7 +750,6 @@ var VirtualScrollMixin = { start: start, stop: stop }); - console.log(start, stop); }, renderRows: function(elems){ var rows = []; @@ -1093,12 +1097,11 @@ var FlowTable = React.createClass({displayName: 'FlowTable', rowHeight: ROW_HEIGHT }; }, - onScroll2: function () { + onScrollFlowTable: function () { this.adjustHead(); this.onScroll(); }, onChange: function () { - console.log("onChange"); this.forceUpdate(); }, scrollIntoView: function (flow) { @@ -1118,12 +1121,13 @@ var FlowTable = React.createClass({displayName: 'FlowTable', ); }, render: function () { + //console.log("render flowtable", this.state.start, this.state.stop, this.props.selected); var flows = this.props.view ? this.props.view.flows : []; var rows = this.renderRows(flows); return ( - React.createElement("div", {className: "flow-table", onScroll: this.onScroll2}, + React.createElement("div", {className: "flow-table", onScroll: this.onScrollFlowTable}, React.createElement("table", null, React.createElement(FlowTableHead, {ref: "head", columns: this.state.columns}), @@ -1187,7 +1191,7 @@ var FlowDetailRequest = React.createClass({displayName: 'FlowDetailRequest', var first_line = [ flow.request.method, RequestUtils.pretty_url(flow.request), - "HTTP/" + flow.response.httpversion.join(".") + "HTTP/" + flow.request.httpversion.join(".") ].join(" "); var content = null; if (flow.request.contentLength > 0) { @@ -1237,6 +1241,20 @@ var FlowDetailResponse = React.createClass({displayName: 'FlowDetailResponse', } }); +var FlowDetailError = React.createClass({displayName: 'FlowDetailError', + render: function () { + var flow = this.props.flow; + return ( + React.createElement("section", null, + React.createElement("div", {className: "alert alert-warning"}, + flow.error.msg, + React.createElement("div", null, React.createElement("small", null, formatTimeStamp(flow.error.timestamp) )) + ) + ) + ); + } +}); + var TimeStamp = React.createClass({displayName: 'TimeStamp', render: function () { @@ -1245,8 +1263,7 @@ var TimeStamp = React.createClass({displayName: 'TimeStamp', return React.createElement("tr", null); } - var ts = (new Date(this.props.t * 1000)).toISOString(); - ts = ts.replace("T", " ").replace("Z", ""); + var ts = formatTimeStamp(this.props.t); var delta; if (this.props.deltaTo) { @@ -1413,24 +1430,31 @@ var FlowDetailConnectionInfo = React.createClass({displayName: 'FlowDetailConnec } }); -var tabs = { +var allTabs = { request: FlowDetailRequest, response: FlowDetailResponse, + error: FlowDetailError, details: FlowDetailConnectionInfo }; var FlowDetail = React.createClass({displayName: 'FlowDetail', - getDefaultProps: function () { - return { - tabs: ["request", "response", "details"] - }; - }, mixins: [StickyHeadMixin, ReactRouter.Navigation, ReactRouter.State], + getTabs: function (flow) { + var tabs = []; + ["request", "response", "error"].forEach(function (e) { + if (flow[e]) { + tabs.push(e); + } + }); + tabs.push("details"); + return tabs; + }, nextTab: function (i) { - var currentIndex = this.props.tabs.indexOf(this.props.active); + var tabs = this.getTabs(); + var currentIndex = tabs.indexOf(this.getParams().detailTab); // JS modulo operator doesn't correct negative numbers, make sure that we are positive. - var nextIndex = (currentIndex + i + this.props.tabs.length) % this.props.tabs.length; - this.selectTab(this.props.tabs[nextIndex]); + var nextIndex = (currentIndex + i + tabs.length) % tabs.length; + this.selectTab(tabs[nextIndex]); }, selectTab: function (panel) { this.replaceWith( @@ -1442,14 +1466,29 @@ var FlowDetail = React.createClass({displayName: 'FlowDetail', ); }, render: function () { - var Tab = tabs[this.props.active]; + var flow = this.props.flow; + var tabs = this.getTabs(flow); + var active = this.getParams().detailTab; + + if (!_.contains(tabs, active)) { + if (active === "response" && flow.error) { + active = "error"; + } else if (active === "error" && flow.response) { + active = "response"; + } else { + active = tabs[0]; + } + this.selectTab(active); + } + + var Tab = allTabs[active]; return ( React.createElement("div", {className: "flow-detail", onScroll: this.adjustHead}, React.createElement(FlowDetailNav, {ref: "head", - tabs: this.props.tabs, - active: this.props.active, + tabs: tabs, + active: active, selectTab: this.selectTab}), - React.createElement(Tab, {flow: this.props.flow}) + React.createElement(Tab, {flow: flow}) ) ); } @@ -1472,6 +1511,21 @@ var MainView = React.createClass({displayName: 'MainView', this.setState({ view: view }); + + view.addListener("recalculate", this.onRecalculate); + view.addListener("add update remove", this.onUpdate); + }, + onRecalculate: function(){ + this.forceUpdate(); + var selected = this.getSelected(); + if(selected){ + this.refs.flowTable.scrollIntoView(); + } + }, + onUpdate: function (flow) { + if (flow.id === this.getParams().flowId) { + this.forceUpdate(); + } }, closeView: function () { this.state.view.close(); @@ -1559,16 +1613,18 @@ var MainView = React.createClass({displayName: 'MainView', } e.preventDefault(); }, + getSelected: function(){ + return this.props.flowStore.get(this.getParams().flowId); + }, render: function () { - var selected = this.props.flowStore.get(this.getParams().flowId); + var selected = this.getSelected(); var details; if (selected) { - details = ( - React.createElement(FlowDetail, {ref: "flowDetails", - flow: selected, - active: this.getParams().detailTab}) - ); + details = [ + React.createElement(Splitter, {key: "splitter"}), + React.createElement(FlowDetail, {key: "flowDetails", ref: "flowDetails", flow: selected}) + ]; } else { details = null; } @@ -1579,7 +1635,6 @@ var MainView = React.createClass({displayName: 'MainView', view: this.state.view, selectFlow: this.selectFlow, selected: selected}), - details ? React.createElement(Splitter, null) : null, details ) ); -- cgit v1.2.3 From 14a8d2f5b83a1ea28abbb490f6c94c43b4e1f960 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 9 Dec 2014 18:18:14 +0100 Subject: always use the app dispatcher --- libmproxy/web/static/js/app.js | 167 ++++++++++++++++++++++------------------- 1 file changed, 91 insertions(+), 76 deletions(-) (limited to 'libmproxy/web/static/js/app.js') diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index f2b8fe4a..16c7ffba 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -102,15 +102,40 @@ AppDispatcher.dispatchServerAction = function (action) { }; var ActionTypes = { - //Settings + // Channel + CHANNEL_OPEN: "channel_open", + CHANNEL_CLOSE: "channel_close", + CHANNEL_ERROR: "channel_error", + + // Settings UPDATE_SETTINGS: "update_settings", - //EventLog + // EventLog ADD_EVENT: "add_event", - //Flow + // Flow ADD_FLOW: "add_flow", UPDATE_FLOW: "update_flow", + REMOVE_FLOW: "remove_flow", + RESET_FLOWS: "reset_flows", +}; + +var ConnectionActions = { + open: function () { + AppDispatcher.dispatchViewAction({ + type: ActionTypes.CHANNEL_OPEN + }); + }, + close: function () { + AppDispatcher.dispatchViewAction({ + type: ActionTypes.CHANNEL_CLOSE + }); + }, + error: function () { + AppDispatcher.dispatchViewAction({ + type: ActionTypes.CHANNEL_ERROR + }); + } }; var SettingsActions = { @@ -126,7 +151,7 @@ var SettingsActions = { } }; -var event_id = 0; +var EventLogActions_event_id = 0; var EventLogActions = { add_event: function (message) { AppDispatcher.dispatchViewAction({ @@ -134,7 +159,7 @@ var EventLogActions = { data: { message: message, level: "web", - id: "viewAction-" + event_id++ + id: "viewAction-" + EventLogActions_event_id++ } }); } @@ -344,7 +369,7 @@ _.extend(_EventLogStore.prototype, EventEmitter.prototype, { var EventLogStore = new _EventLogStore(); AppDispatcher.register(EventLogStore.handle.bind(EventLogStore)); -function FlowStore(endpoint) { +function FlowStore() { this._views = []; this.reset(); } @@ -389,21 +414,46 @@ _.extend(FlowStore.prototype, { }); -function LiveFlowStore(endpoint) { +function LiveFlowStore() { FlowStore.call(this); this.updates_before_fetch = undefined; this._fetchxhr = false; - this.endpoint = endpoint || "/flows"; - this.conn = new Connection(this.endpoint + "/updates"); - this.conn.onopen = this._onopen.bind(this); - this.conn.onmessage = function (e) { - var message = JSON.parse(e.data); - this.handle_update(message.type, message.data); - }.bind(this); + + this.handle = this.handle.bind(this); + AppDispatcher.register(this.handle); + + // Avoid double-fetch on startup. + if(!(window.ws && window.ws.readyState === WebSocket.CONNECTING)) { + this.fetch(); + } } _.extend(LiveFlowStore.prototype, FlowStore.prototype, { + handle: function (event) { + switch (event.type) { + case ActionTypes.CHANNEL_OPEN: + case ActionTypes.RESET_FLOWS: + this.fetch(); + break; + case ActionTypes.ADD_FLOW: + case ActionTypes.UPDATE_FLOW: + case ActionTypes.REMOVE_FLOW: + if (this.updates_before_fetch) { + console.log("defer update", type, data); + this.updates_before_fetch.push(event); + } else { + if(event.type === ActionTypes.ADD_FLOW){ + this.add(event.data); + } else if (event.type === ActionTypes.UPDATE_FLOW){ + this.update(event.data); + } else { + this.remove(event.data); + } + } + break; + } + }, close: function () { - this.conn.close(); + AppDispatcher.unregister(this.handle); }, add: function (flow) { // Make sure that deferred adds don't add an element twice. @@ -411,32 +461,14 @@ _.extend(LiveFlowStore.prototype, FlowStore.prototype, { FlowStore.prototype.add.call(this, flow); } }, - _onopen: function () { - //Update stream openend, fetch list of flows. - console.log("Update Connection opened, fetching flows..."); - this.fetch(); - }, fetch: function () { + console.log("fetch"); if (this._fetchxhr) { this._fetchxhr.abort(); } - this._fetchxhr = $.getJSON(this.endpoint, this.handle_fetch.bind(this)); + this._fetchxhr = $.getJSON("/flows", this.handle_fetch.bind(this)); this.updates_before_fetch = []; // (JS: empty array is true) }, - handle_update: function (type, data) { - console.log("LiveFlowStore.handle_update", type, data); - - if (type === "reset") { - return this.fetch(); - } - - if (this.updates_before_fetch) { - console.log("defer update", type, data); - this.updates_before_fetch.push(arguments); - } else { - this[type](data); - } - }, handle_fetch: function (data) { this._fetchxhr = false; console.log("Flows fetched.", this.updates_before_fetch); @@ -444,7 +476,7 @@ _.extend(LiveFlowStore.prototype, FlowStore.prototype, { var updates = this.updates_before_fetch; this.updates_before_fetch = false; for (var i = 0; i < updates.length; i++) { - this.handle_update.apply(this, updates[i]); + this.handle(updates[i]); } }, }); @@ -550,44 +582,30 @@ _.extend(FlowView.prototype, EventEmitter.prototype, { } } }); -function Connection(url) { - if (url[0] != "/") { - this.url = url; - } else { - this.url = location.origin.replace("http", "ws") + url; +function Channel(url) { + + if (url[0] === "/") { + url = location.origin.replace("http", "ws") + url; } - var ws = new WebSocket(this.url); + + var ws = new WebSocket(url); ws.onopen = function () { - this.onopen.apply(this, arguments); - }.bind(this); - ws.onmessage = function () { - this.onmessage.apply(this, arguments); - }.bind(this); + ConnectionActions.open(); + }; + ws.onmessage = function (message) { + var m = JSON.parse(message.data); + AppDispatcher.dispatchServerAction(m); + }; ws.onerror = function () { - this.onerror.apply(this, arguments); - }.bind(this); + ConnectionActions.error(); + EventLogActions.add_event("WebSocket connection error."); + }; ws.onclose = function () { - this.onclose.apply(this, arguments); - }.bind(this); - this.ws = ws; + ConnectionActions.close(); + EventLogActions.add_event("WebSocket connection closed."); + }; + return ws; } -Connection.prototype.onopen = function (open) { - console.debug("onopen", this, arguments); -}; -Connection.prototype.onmessage = function (message) { - console.warn("onmessage (not implemented)", this, message.data); -}; -Connection.prototype.onerror = function (error) { - EventLogActions.add_event("WebSocket Connection Error."); - console.debug("onerror", this, arguments); -}; -Connection.prototype.onclose = function (close) { - EventLogActions.add_event("WebSocket Connection closed."); - console.debug("onclose", this, arguments); -}; -Connection.prototype.close = function () { - this.ws.close(); -}; //React utils. For other utilities, see ../utils.js var Splitter = React.createClass({displayName: 'Splitter', @@ -1450,7 +1468,7 @@ var FlowDetail = React.createClass({displayName: 'FlowDetail', return tabs; }, nextTab: function (i) { - var tabs = this.getTabs(); + var tabs = this.getTabs(this.props.flow); var currentIndex = tabs.indexOf(this.getParams().detailTab); // JS modulo operator doesn't correct negative numbers, make sure that we are positive. var nextIndex = (currentIndex + i + tabs.length) % tabs.length; @@ -1519,7 +1537,7 @@ var MainView = React.createClass({displayName: 'MainView', this.forceUpdate(); var selected = this.getSelected(); if(selected){ - this.refs.flowTable.scrollIntoView(); + this.refs.flowTable.scrollIntoView(selected); } }, onUpdate: function (flow) { @@ -1841,13 +1859,10 @@ var routes = ( ) ); $(function () { + window.ws = new Channel("/updates"); + ReactRouter.run(routes, function (Handler) { React.render(React.createElement(Handler, null), document.body); }); - var UpdateConnection = new Connection("/updates"); - UpdateConnection.onmessage = function (message) { - var m = JSON.parse(message.data); - AppDispatcher.dispatchServerAction(m); - }; }); //# sourceMappingURL=app.js.map \ No newline at end of file -- cgit v1.2.3 From 05bc7e8cd8382aabdd44f7bc569d2fd421c26f21 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 9 Dec 2014 18:55:16 +0100 Subject: generalize store --- libmproxy/web/static/js/app.js | 241 +++++++++++++++++++++-------------------- 1 file changed, 122 insertions(+), 119 deletions(-) (limited to 'libmproxy/web/static/js/app.js') diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index 16c7ffba..6596b121 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -102,10 +102,10 @@ AppDispatcher.dispatchServerAction = function (action) { }; var ActionTypes = { - // Channel - CHANNEL_OPEN: "channel_open", - CHANNEL_CLOSE: "channel_close", - CHANNEL_ERROR: "channel_error", + // Connection + CONNECTION_OPEN: "connection_open", + CONNECTION_CLOSE: "connection_close", + CONNECTION_ERROR: "connection_error", // Settings UPDATE_SETTINGS: "update_settings", @@ -123,17 +123,17 @@ var ActionTypes = { var ConnectionActions = { open: function () { AppDispatcher.dispatchViewAction({ - type: ActionTypes.CHANNEL_OPEN + type: ActionTypes.CONNECTION_OPEN }); }, close: function () { AppDispatcher.dispatchViewAction({ - type: ActionTypes.CHANNEL_CLOSE + type: ActionTypes.CONNECTION_CLOSE }); }, error: function () { AppDispatcher.dispatchViewAction({ - type: ActionTypes.CHANNEL_ERROR + type: ActionTypes.CONNECTION_ERROR }); } }; @@ -241,6 +241,117 @@ EventEmitter.prototype.removeListener = function (events, f) { }.bind(this)); }; + +function Store() { + this._views = []; + this.reset(); +} +_.extend(Store.prototype, { + add: function (elem) { + if (elem.id in this._pos_map) { + return; + } + + this._pos_map[elem.id] = this._list.length; + this._list.push(elem); + for (var i = 0; i < this._views.length; i++) { + this._views[i].add(elem); + } + }, + update: function (elem) { + if (!(elem.id in this._pos_map)) { + return; + } + + this._list[this._pos_map[elem.id]] = elem; + for (var i = 0; i < this._views.length; i++) { + this._views[i].update(elem); + } + }, + remove: function (elem_id) { + if (!(elem.id in this._pos_map)) { + return; + } + + this._list.splice(this._pos_map[elem_id], 1); + this._build_map(); + for (var i = 0; i < this._views.length; i++) { + this._views[i].remove(elem_id); + } + }, + reset: function (elems) { + this._list = elems || []; + this._build_map(); + for (var i = 0; i < this._views.length; i++) { + this._views[i].recalculate(this._list); + } + }, + _build_map: function () { + this._pos_map = {}; + for (var i = 0; i < this._list.length; i++) { + var elem = this._list[i]; + this._pos_map[elem.id] = i; + } + }, + get: function (elem_id) { + return this._list[this._pos_map[elem_id]]; + } +}); + + +function LiveStore(type) { + Store.call(this); + this.type = type; + + this._updates_before_fetch = undefined; + this._fetchxhr = false; + + this.handle = this.handle.bind(this); + AppDispatcher.register(this.handle); + + // Avoid double-fetch on startup. + if (!(window.ws && window.ws.readyState === WebSocket.CONNECTING)) { + this.fetch(); + } +} +_.extend(LiveStore.prototype, Store.prototype, { + handle: function (event) { + if (event.type === ActionTypes.CONNECTION_OPEN) { + return this.fetch(); + } + if (event.type === this.type) { + if (event.cmd === "reset") { + this.fetch(); + } else if (this._updates_before_fetch) { + console.log("defer update", event); + this._updates_before_fetch.push(event); + } else { + this[event.cmd](event.data); + } + } + }, + close: function () { + AppDispatcher.unregister(this.handle); + }, + fetch: function () { + console.log("fetch " + this.type); + if (this._fetchxhr) { + this._fetchxhr.abort(); + } + this._fetchxhr = $.getJSON("/" + this.type, this.handle_fetch.bind(this)); + this._updates_before_fetch = []; // (JS: empty array is true) + }, + handle_fetch: function (data) { + this._fetchxhr = false; + console.log(this.type + " fetched.", this._updates_before_fetch); + this.reset(data.flows); + var updates = this._updates_before_fetch; + this._updates_before_fetch = false; + for (var i = 0; i < updates.length; i++) { + this.handle(updates[i]); + } + }, +}); function _SettingsStore() { EventEmitter.call(this); @@ -369,117 +480,9 @@ _.extend(_EventLogStore.prototype, EventEmitter.prototype, { var EventLogStore = new _EventLogStore(); AppDispatcher.register(EventLogStore.handle.bind(EventLogStore)); -function FlowStore() { - this._views = []; - this.reset(); -} -_.extend(FlowStore.prototype, { - add: function (flow) { - this._pos_map[flow.id] = this._flow_list.length; - this._flow_list.push(flow); - for (var i = 0; i < this._views.length; i++) { - this._views[i].add(flow); - } - }, - update: function (flow) { - this._flow_list[this._pos_map[flow.id]] = flow; - for (var i = 0; i < this._views.length; i++) { - this._views[i].update(flow); - } - }, - remove: function (flow_id) { - this._flow_list.splice(this._pos_map[flow_id], 1); - this._build_map(); - for (var i = 0; i < this._views.length; i++) { - this._views[i].remove(flow_id); - } - }, - reset: function (flows) { - this._flow_list = flows || []; - this._build_map(); - for (var i = 0; i < this._views.length; i++) { - this._views[i].recalculate(this._flow_list); - } - }, - _build_map: function () { - this._pos_map = {}; - for (var i = 0; i < this._flow_list.length; i++) { - var flow = this._flow_list[i]; - this._pos_map[flow.id] = i; - } - }, - get: function (flow_id) { - return this._flow_list[this._pos_map[flow_id]]; - } -}); - - function LiveFlowStore() { - FlowStore.call(this); - this.updates_before_fetch = undefined; - this._fetchxhr = false; - - this.handle = this.handle.bind(this); - AppDispatcher.register(this.handle); - - // Avoid double-fetch on startup. - if(!(window.ws && window.ws.readyState === WebSocket.CONNECTING)) { - this.fetch(); - } + return new LiveStore("flows"); } -_.extend(LiveFlowStore.prototype, FlowStore.prototype, { - handle: function (event) { - switch (event.type) { - case ActionTypes.CHANNEL_OPEN: - case ActionTypes.RESET_FLOWS: - this.fetch(); - break; - case ActionTypes.ADD_FLOW: - case ActionTypes.UPDATE_FLOW: - case ActionTypes.REMOVE_FLOW: - if (this.updates_before_fetch) { - console.log("defer update", type, data); - this.updates_before_fetch.push(event); - } else { - if(event.type === ActionTypes.ADD_FLOW){ - this.add(event.data); - } else if (event.type === ActionTypes.UPDATE_FLOW){ - this.update(event.data); - } else { - this.remove(event.data); - } - } - break; - } - }, - close: function () { - AppDispatcher.unregister(this.handle); - }, - add: function (flow) { - // Make sure that deferred adds don't add an element twice. - if (!(flow.id in this._pos_map)) { - FlowStore.prototype.add.call(this, flow); - } - }, - fetch: function () { - console.log("fetch"); - if (this._fetchxhr) { - this._fetchxhr.abort(); - } - this._fetchxhr = $.getJSON("/flows", this.handle_fetch.bind(this)); - this.updates_before_fetch = []; // (JS: empty array is true) - }, - handle_fetch: function (data) { - this._fetchxhr = false; - console.log("Flows fetched.", this.updates_before_fetch); - this.reset(data.flows); - var updates = this.updates_before_fetch; - this.updates_before_fetch = false; - for (var i = 0; i < updates.length; i++) { - this.handle(updates[i]); - } - }, -}); function SortByInsertionOrder() { this.i = 0; @@ -505,7 +508,7 @@ function FlowView(store, filt, sortfun) { this.store = store; this.store._views.push(this); - this.recalculate(this.store._flow_list, filt, sortfun); + this.recalculate(this.store._list, filt, sortfun); } _.extend(FlowView.prototype, EventEmitter.prototype, { @@ -582,7 +585,7 @@ _.extend(FlowView.prototype, EventEmitter.prototype, { } } }); -function Channel(url) { +function Connection(url) { if (url[0] === "/") { url = location.origin.replace("http", "ws") + url; @@ -1859,7 +1862,7 @@ var routes = ( ) ); $(function () { - window.ws = new Channel("/updates"); + window.ws = new Connection("/updates"); ReactRouter.run(routes, function (Handler) { React.render(React.createElement(Handler, null), document.body); -- cgit v1.2.3 From e12bf19e35867f3ea69f45054decb024a75fc2b4 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 10 Dec 2014 00:47:05 +0100 Subject: web: add event store, fix all those bugs --- libmproxy/web/static/js/app.js | 337 +++++++++++++++-------------------------- 1 file changed, 122 insertions(+), 215 deletions(-) (limited to 'libmproxy/web/static/js/app.js') diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index 6596b121..b44633bd 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -78,9 +78,9 @@ Dispatcher.prototype.register = function (callback) { this.callbacks.push(callback); }; Dispatcher.prototype.unregister = function (callback) { - var index = this.callbacks.indexOf(f); + var index = this.callbacks.indexOf(callback); if (index >= 0) { - this.callbacks.splice(this.callbacks.indexOf(f), 1); + this.callbacks.splice(index, 1); } }; Dispatcher.prototype.dispatch = function (payload) { @@ -111,13 +111,18 @@ var ActionTypes = { UPDATE_SETTINGS: "update_settings", // EventLog + EVENT_STORE: "events", ADD_EVENT: "add_event", // Flow - ADD_FLOW: "add_flow", - UPDATE_FLOW: "update_flow", - REMOVE_FLOW: "remove_flow", - RESET_FLOWS: "reset_flows", + FLOW_STORE: "flows", +}; + +var StoreCmds = { + ADD: "add", + UPDATE: "update", + REMOVE: "remove", + RESET: "reset" }; var ConnectionActions = { @@ -155,7 +160,8 @@ var EventLogActions_event_id = 0; var EventLogActions = { add_event: function (message) { AppDispatcher.dispatchViewAction({ - type: ActionTypes.ADD_EVENT, + type: ActionTypes.EVENT_STORE, + cmd: StoreCmds.ADD, data: { message: message, level: "web", @@ -295,6 +301,9 @@ _.extend(Store.prototype, { }, get: function (elem_id) { return this._list[this._pos_map[elem_id]]; + }, + index: function(elem_id) { + return this._pos_map[elem_id]; } }); @@ -320,7 +329,7 @@ _.extend(LiveStore.prototype, Store.prototype, { return this.fetch(); } if (event.type === this.type) { - if (event.cmd === "reset") { + if (event.cmd === StoreCmds.RESET) { this.fetch(); } else if (this._updates_before_fetch) { console.log("defer update", event); @@ -344,7 +353,7 @@ _.extend(LiveStore.prototype, Store.prototype, { handle_fetch: function (data) { this._fetchxhr = false; console.log(this.type + " fetched.", this._updates_before_fetch); - this.reset(data.flows); + this.reset(data.list); var updates = this._updates_before_fetch; this._updates_before_fetch = false; for (var i = 0; i < updates.length; i++) { @@ -352,158 +361,20 @@ _.extend(LiveStore.prototype, Store.prototype, { } }, }); -function _SettingsStore() { - EventEmitter.call(this); - - //FIXME: What do we do if we haven't requested anything from the server yet? - this.settings = { - version: "0.12", - showEventLog: true, - mode: "transparent", - }; -} -_.extend(_SettingsStore.prototype, EventEmitter.prototype, { - getAll: function () { - return this.settings; - }, - handle: function (action) { - switch (action.type) { - case ActionTypes.UPDATE_SETTINGS: - this.settings = action.settings; - this.emit("change"); - break; - default: - return; - } - } -}); - -var SettingsStore = new _SettingsStore(); -AppDispatcher.register(SettingsStore.handle.bind(SettingsStore)); - -// -// We have an EventLogView and an EventLogStore: -// The basic architecture is that one can request views on the event log -// from the store, which returns a view object and then deals with getting the data required for the view. -// The view object is accessed by React components and distributes updates etc. -// -// See also: components/EventLog.react.js -function EventLogView(store, live) { - EventEmitter.call(this); - this._store = store; - this.live = live; - this.log = []; - - this.add = this.add.bind(this); - - if (live) { - this._store.addListener(ActionTypes.ADD_EVENT, this.add); - } -} -_.extend(EventLogView.prototype, EventEmitter.prototype, { - close: function () { - this._store.removeListener(ActionTypes.ADD_EVENT, this.add); - }, - getAll: function () { - return this.log; - }, - add: function (entry) { - this.log.push(entry); - if (this.log.length > 200) { - this.log.shift(); - } - this.emit("change"); - }, - add_bulk: function (messages) { - var log = messages; - var last_id = log[log.length - 1].id; - var to_add = _.filter(this.log, function (entry) { - return entry.id > last_id; - }); - this.log = log.concat(to_add); - this.emit("change"); - } -}); -function _EventLogStore() { - EventEmitter.call(this); +function SortByStoreOrder(elem) { + return this.store.index(elem.id); } -_.extend(_EventLogStore.prototype, EventEmitter.prototype, { - getView: function (since) { - var view = new EventLogView(this, !since); - return view; - /* - //TODO: Really do bulk retrieval of last messages. - window.setTimeout(function () { - view.add_bulk([ - { - id: 1, - message: "Hello World" - }, - { - id: 2, - message: "I was already transmitted as an event." - } - ]); - }, 100); - - var id = 2; - view.add({ - id: id++, - message: "I was already transmitted as an event." - }); - view.add({ - id: id++, - message: "I was only transmitted as an event before the bulk was added.." - }); - window.setInterval(function () { - view.add({ - id: id++, - message: "." - }); - }, 1000); - return view; - */ - }, - handle: function (action) { - switch (action.type) { - case ActionTypes.ADD_EVENT: - this.emit(ActionTypes.ADD_EVENT, action.data); - break; - default: - return; - } - } -}); - -var EventLogStore = new _EventLogStore(); -AppDispatcher.register(EventLogStore.handle.bind(EventLogStore)); -function LiveFlowStore() { - return new LiveStore("flows"); -} - -function SortByInsertionOrder() { - this.i = 0; - this.map = {}; - this.key = this.key.bind(this); -} -SortByInsertionOrder.prototype.key = function (flow) { - if (!(flow.id in this.map)) { - this.i++; - this.map[flow.id] = this.i; - } - return this.map[flow.id]; +var default_sort = SortByStoreOrder; +var default_filt = function(elem){ + return true; }; -var default_sort = (new SortByInsertionOrder()).key; - -function FlowView(store, filt, sortfun) { +function StoreView(store, filt, sortfun) { EventEmitter.call(this); - filt = filt || function (flow) { - return true; - }; + filt = filt || default_filt; sortfun = sortfun || default_sort; this.store = store; @@ -511,80 +382,113 @@ function FlowView(store, filt, sortfun) { this.recalculate(this.store._list, filt, sortfun); } -_.extend(FlowView.prototype, EventEmitter.prototype, { +_.extend(StoreView.prototype, EventEmitter.prototype, { close: function () { this.store._views = _.without(this.store._views, this); }, - recalculate: function (flows, filt, sortfun) { + recalculate: function (elems, filt, sortfun) { if (filt) { this.filt = filt; } if (sortfun) { - this.sortfun = sortfun; - } - - //Ugly workaround: Call .sortfun() for each flow once in order, - //so that SortByInsertionOrder make sense. - for (var i = 0; i < flows.length; i++) { - this.sortfun(flows[i]); + this.sortfun = sortfun.bind(this); } - this.flows = flows.filter(this.filt); - this.flows.sort(function (a, b) { + this.list = elems.filter(this.filt); + this.list.sort(function (a, b) { return this.sortfun(a) - this.sortfun(b); }.bind(this)); this.emit("recalculate"); }, - index: function (flow) { - return _.sortedIndex(this.flows, flow, this.sortfun); + index: function (elem) { + return _.sortedIndex(this.list, elem, this.sortfun); }, - add: function (flow) { - if (this.filt(flow)) { - var idx = this.index(flow); - if (idx === this.flows.length) { //happens often, .push is way faster. - this.flows.push(flow); + add: function (elem) { + if (this.filt(elem)) { + var idx = this.index(elem); + if (idx === this.list.length) { //happens often, .push is way faster. + this.list.push(elem); } else { - this.flows.splice(idx, 0, flow); + this.list.splice(idx, 0, elem); } - this.emit("add", flow, idx); + this.emit("add", elem, idx); } }, - update: function (flow) { + update: function (elem) { var idx; - var i = this.flows.length; - // Search from the back, we usually update the latest flows. + var i = this.list.length; + // Search from the back, we usually update the latest entries. while (i--) { - if (this.flows[i].id === flow.id) { + if (this.list[i].id === elem.id) { idx = i; break; } } if (idx === -1) { //not contained in list - this.add(flow); - } else if (!this.filt(flow)) { - this.remove(flow.id); + this.add(elem); + } else if (!this.filt(elem)) { + this.remove(elem.id); } else { - if (this.sortfun(this.flows[idx]) !== this.sortfun(flow)) { //sortpos has changed - this.remove(this.flows[idx]); - this.add(flow); + if (this.sortfun(this.list[idx]) !== this.sortfun(elem)) { //sortpos has changed + this.remove(this.list[idx]); + this.add(elem); } else { - this.flows[idx] = flow; - this.emit("update", flow, idx); + this.list[idx] = elem; + this.emit("update", elem, idx); } } }, - remove: function (flow_id) { - var i = this.flows.length; - while (i--) { - if (this.flows[i].id === flow_id) { - this.flows.splice(i, 1); - this.emit("remove", flow_id, i); + remove: function (elem_id) { + var idx = this.list.length; + while (idx--) { + if (this.list[idx].id === elem_id) { + this.list.splice(idx, 1); + this.emit("remove", elem_id, idx); break; } } } }); + + +function FlowStore() { + return new LiveStore(ActionTypes.FLOW_STORE); +} +function EventLogStore() { + return new LiveStore(ActionTypes.EVENT_STORE); +} +function _SettingsStore() { + EventEmitter.call(this); + + //FIXME: What do we do if we haven't requested anything from the server yet? + this.settings = { + version: "0.12", + showEventLog: true, + mode: "transparent", + }; +} +_.extend(_SettingsStore.prototype, EventEmitter.prototype, { + getAll: function () { + return this.settings; + }, + handle: function (action) { + switch (action.type) { + case ActionTypes.UPDATE_SETTINGS: + this.settings = action.settings; + this.emit("change"); + break; + default: + return; + } + } +}); + +var SettingsStore = new _SettingsStore(); +AppDispatcher.register(SettingsStore.handle.bind(SettingsStore)); + + + function Connection(url) { if (url[0] === "/") { @@ -736,10 +640,12 @@ var VirtualScrollMixin = { console.warn("VirtualScrollMixin: No rowHeight specified", this); } }, - getPlaceholderTop: function () { + getPlaceholderTop: function (total) { var Tag = this.props.placeholderTagName || "tr"; + // When a large trunk of elements is removed from the button, start may be far off the viewport. + // To make this issue less severe, limit the top placeholder to the total number of rows. var style = { - height: this.state.start * this.props.rowHeight + height: Math.min(this.state.start , total) * this.props.rowHeight }; var spacer = React.createElement(Tag, {key: "placeholder-top", style: style}); @@ -1143,7 +1049,7 @@ var FlowTable = React.createClass({displayName: 'FlowTable', }, render: function () { //console.log("render flowtable", this.state.start, this.state.stop, this.props.selected); - var flows = this.props.view ? this.props.view.flows : []; + var flows = this.props.view ? this.props.view.list : []; var rows = this.renderRows(flows); @@ -1153,7 +1059,7 @@ var FlowTable = React.createClass({displayName: 'FlowTable', React.createElement(FlowTableHead, {ref: "head", columns: this.state.columns}), React.createElement("tbody", {ref: "body"}, - this.getPlaceholderTop(), + this.getPlaceholderTop(flows.length), rows, this.getPlaceholderBottom(flows.length) ) @@ -1528,7 +1434,7 @@ var MainView = React.createClass({displayName: 'MainView', } }, openView: function (store) { - var view = new FlowView(store); + var view = new StoreView(store); this.setState({ view: view }); @@ -1688,32 +1594,33 @@ var LogMessage = React.createClass({displayName: 'LogMessage', var EventLogContents = React.createClass({displayName: 'EventLogContents', mixins: [AutoScrollMixin, VirtualScrollMixin], - getInitialState: function () { + getInitialState: function(){ + var store = new EventLogStore(); + var view = new StoreView(store, function(entry){ + return this.props.filter[entry.level]; + }.bind(this)); + view.addListener("add recalculate", this.onEventLogChange); return { + store: store, + view: view, log: [] }; }, - componentDidMount: function () { - this.log = EventLogStore.getView(); - this.log.addListener("change", this.onEventLogChange); - }, componentWillUnmount: function () { - this.log.removeListener("change", this.onEventLogChange); - this.log.close(); + this.state.view.removeListener("add recalculate", this.onEventLogChange); + this.state.view.close(); + this.state.store.close(); }, onEventLogChange: function () { - var log = this.log.getAll().filter(function (entry) { - return this.props.filter[entry.level]; - }.bind(this)); this.setState({ - log: log + log: this.state.view.list }); }, - componentWillReceiveProps: function () { - if (this.log) { - this.onEventLogChange(); + componentWillReceiveProps: function (nextProps) { + if(nextProps.filter !== this.props.filter){ + this.props.filter = nextProps.filter; // Dirty: Make sure that view filter sees the update. + this.state.view.recalculate(this.state.store._list); } - }, getDefaultProps: function () { return { @@ -1729,7 +1636,7 @@ var EventLogContents = React.createClass({displayName: 'EventLogContents', var rows = this.renderRows(this.state.log); return React.createElement("pre", {onScroll: this.onScroll}, - this.getPlaceholderTop(), + this.getPlaceholderTop(this.state.log.length), rows, this.getPlaceholderBottom(this.state.log.length) ); @@ -1775,7 +1682,7 @@ var EventLog = React.createClass({displayName: 'EventLog', }); }, toggleLevel: function (level) { - var filter = this.state.filter; + var filter = _.extend({}, this.state.filter); filter[level] = !filter[level]; this.setState({filter: filter}); }, @@ -1820,7 +1727,7 @@ var ProxyAppMain = React.createClass({displayName: 'ProxyAppMain', getInitialState: function () { return { settings: SettingsStore.getAll(), - flowStore: new LiveFlowStore() + flowStore: new FlowStore() }; }, componentDidMount: function () { -- cgit v1.2.3 From d2feaf5d84820e75e3931522d889748563972c75 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 10 Dec 2014 02:48:04 +0100 Subject: web: take viewport resize into account --- libmproxy/web/static/js/app.js | 129 ++++++++++++++++++++++++----------------- 1 file changed, 77 insertions(+), 52 deletions(-) (limited to 'libmproxy/web/static/js/app.js') diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index b44633bd..a761aaad 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -12,6 +12,7 @@ var AutoScrollMixin = { }, }; + var StickyHeadMixin = { adjustHead: function () { // Abusing CSS transforms to set the element @@ -21,6 +22,7 @@ var StickyHeadMixin = { } }; + var Key = { UP: 38, DOWN: 40, @@ -38,6 +40,7 @@ var Key = { L: 76 }; + var formatSize = function (bytes) { var size = bytes; var prefix = ["B", "KB", "MB", "GB", "TB"]; @@ -49,6 +52,7 @@ var formatSize = function (bytes) { return (Math.floor(size * 100) / 100.0).toFixed(2) + prefix[i]; }; + var formatTimeDelta = function (milliseconds) { var time = milliseconds; var prefix = ["ms", "s", "min", "h"]; @@ -61,10 +65,42 @@ var formatTimeDelta = function (milliseconds) { return Math.round(time) + prefix[i]; }; + var formatTimeStamp = function (seconds) { var ts = (new Date(seconds * 1000)).toISOString(); return ts.replace("T", " ").replace("Z", ""); }; + + +function EventEmitter() { + this.listeners = {}; +} +EventEmitter.prototype.emit = function (event) { + if (!(event in this.listeners)) { + return; + } + var args = Array.prototype.slice.call(arguments, 1); + this.listeners[event].forEach(function (listener) { + listener.apply(this, args); + }.bind(this)); +}; +EventEmitter.prototype.addListener = function (events, f) { + events.split(" ").forEach(function (event) { + this.listeners[event] = this.listeners[event] || []; + this.listeners[event].push(f); + }.bind(this)); +}; +EventEmitter.prototype.removeListener = function (events, f) { + if (!(events in this.listeners)) { + return false; + } + events.split(" ").forEach(function (event) { + var index = this.listeners[event].indexOf(f); + if (index >= 0) { + this.listeners[event].splice(index, 1); + } + }.bind(this)); +}; const PayloadSources = { VIEW: "view", SERVER: "server" @@ -217,37 +253,6 @@ var RequestUtils = _.extend(_MessageUtils, { }); var ResponseUtils = _.extend(_MessageUtils, {}); -function EventEmitter() { - this.listeners = {}; -} -EventEmitter.prototype.emit = function (event) { - if (!(event in this.listeners)) { - return; - } - var args = Array.prototype.slice.call(arguments, 1); - this.listeners[event].forEach(function (listener) { - listener.apply(this, args); - }.bind(this)); -}; -EventEmitter.prototype.addListener = function (events, f) { - events.split(" ").forEach(function (event) { - this.listeners[event] = this.listeners[event] || []; - this.listeners[event].push(f); - }.bind(this)); -}; -EventEmitter.prototype.removeListener = function (events, f) { - if (!(events in this.listeners)) { - return false; - } - events.split(" ").forEach(function (event) { - var index = this.listeners[event].indexOf(f); - if (index >= 0) { - this.listeners[event].splice(index, 1); - } - }.bind(this)); -}; - - function Store() { this._views = []; this.reset(); @@ -363,6 +368,14 @@ _.extend(LiveStore.prototype, Store.prototype, { }); +function FlowStore() { + return new LiveStore(ActionTypes.FLOW_STORE); +} + + +function EventLogStore() { + return new LiveStore(ActionTypes.EVENT_STORE); +} function SortByStoreOrder(elem) { return this.store.index(elem.id); } @@ -450,14 +463,6 @@ _.extend(StoreView.prototype, EventEmitter.prototype, { } } }); - - -function FlowStore() { - return new LiveStore(ActionTypes.FLOW_STORE); -} -function EventLogStore() { - return new LiveStore(ActionTypes.EVENT_STORE); -} function _SettingsStore() { EventEmitter.call(this); @@ -487,8 +492,6 @@ _.extend(_SettingsStore.prototype, EventEmitter.prototype, { var SettingsStore = new _SettingsStore(); AppDispatcher.register(SettingsStore.handle.bind(SettingsStore)); - - function Connection(url) { if (url[0] === "/") { @@ -566,6 +569,7 @@ var Splitter = React.createClass({displayName: 'Splitter', this.setState({ applied: true }); + this.onResize(); }, onMouseMove: function (e) { var dX = 0, dY = 0; @@ -576,6 +580,13 @@ var Splitter = React.createClass({displayName: 'Splitter', } this.getDOMNode().style.transform = "translate(" + dX + "px," + dY + "px)"; }, + onResize: function () { + // Trigger a global resize event. This notifies components that employ virtual scrolling + // that their viewport may have changed. + window.setTimeout(function () { + window.dispatchEvent(new CustomEvent("resize")); + }, 1); + }, reset: function (willUnmount) { if (!this.state.applied) { return; @@ -592,7 +603,7 @@ var Splitter = React.createClass({displayName: 'Splitter', applied: false }); } - + this.onResize(); }, componentWillUnmount: function () { this.reset(true); @@ -619,9 +630,9 @@ function getCookie(name) { var xsrf = $.param({_xsrf: getCookie("_xsrf")}); //Tornado XSRF Protection. -$.ajaxPrefilter(function(options){ - if(options.type === "post" && options.url[0] === "/"){ - if(options.data){ +$.ajaxPrefilter(function (options) { + if (options.type === "post" && options.url[0] === "/") { + if (options.data) { options.data += ("&" + xsrf); } else { options.data = xsrf; @@ -635,8 +646,8 @@ var VirtualScrollMixin = { stop: 0 }; }, - componentWillMount: function(){ - if(!this.props.rowHeight){ + componentWillMount: function () { + if (!this.props.rowHeight) { console.warn("VirtualScrollMixin: No rowHeight specified", this); } }, @@ -645,7 +656,7 @@ var VirtualScrollMixin = { // When a large trunk of elements is removed from the button, start may be far off the viewport. // To make this issue less severe, limit the top placeholder to the total number of rows. var style = { - height: Math.min(this.state.start , total) * this.props.rowHeight + height: Math.min(this.state.start, total) * this.props.rowHeight }; var spacer = React.createElement(Tag, {key: "placeholder-top", style: style}); @@ -665,6 +676,10 @@ var VirtualScrollMixin = { }, componentDidMount: function () { this.onScroll(); + window.addEventListener('resize', this.onScroll); + }, + componentWillUnmount: function(){ + window.removeEventListener('resize', this.onScroll); }, onScroll: function () { var viewport = this.getDOMNode(); @@ -678,7 +693,7 @@ var VirtualScrollMixin = { stop: stop }); }, - renderRows: function(elems){ + renderRows: function (elems) { var rows = []; var max = Math.min(elems.length, this.state.stop); @@ -688,7 +703,7 @@ var VirtualScrollMixin = { } return rows; }, - scrollRowIntoView: function(index, head_height){ + scrollRowIntoView: function (index, head_height) { var row_top = (index * this.props.rowHeight) + head_height; var row_bottom = row_top + this.props.rowHeight; @@ -1740,12 +1755,22 @@ var ProxyAppMain = React.createClass({displayName: 'ProxyAppMain', this.setState({settings: SettingsStore.getAll()}); }, render: function () { + + var eventlog; + if (this.state.settings.showEventLog) { + eventlog = [ + React.createElement(Splitter, {key: "splitter", axis: "y"}), + React.createElement(EventLog, {key: "eventlog"}) + ]; + } else { + eventlog = null; + } + return ( React.createElement("div", {id: "container"}, React.createElement(Header, {settings: this.state.settings}), React.createElement(RouteHandler, {settings: this.state.settings, flowStore: this.state.flowStore}), - this.state.settings.showEventLog ? React.createElement(Splitter, {axis: "y"}) : null, - this.state.settings.showEventLog ? React.createElement(EventLog, null) : null, + eventlog, React.createElement(Footer, {settings: this.state.settings}) ) ); -- cgit v1.2.3 From 7e40b8ab09d6d605307342fbfa21129ca15ff055 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 10 Dec 2014 15:25:40 +0100 Subject: web: implement settings store, modularize store --- libmproxy/web/static/js/app.js | 260 +++++++++++++++++++++++++---------------- 1 file changed, 162 insertions(+), 98 deletions(-) (limited to 'libmproxy/web/static/js/app.js') diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index a761aaad..13b57689 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -72,6 +72,28 @@ var formatTimeStamp = function (seconds) { }; +function listToDict(list, propKey, propVal) { + var dict = {}; + for (var i = 0; i < list.length; i++) { + var e = list[i]; + dict[e[propKey]] = e[propVal]; + } + return dict; +} +function dictToList(dict, propKey, propVal) { + var list = []; + for (var key in dict) { + if (dict.hasOwnProperty(key)) { + var elem = {}; + elem[propKey] = key; + elem[propVal] = dict[key]; + list.push(elem); + } + } + return list; +} + + function EventEmitter() { this.listeners = {}; } @@ -144,11 +166,11 @@ var ActionTypes = { CONNECTION_ERROR: "connection_error", // Settings + SETTINGS_STORE: "settings", UPDATE_SETTINGS: "update_settings", // EventLog EVENT_STORE: "events", - ADD_EVENT: "add_event", // Flow FLOW_STORE: "flows", @@ -181,13 +203,13 @@ var ConnectionActions = { var SettingsActions = { update: function (settings) { - settings = _.merge({}, SettingsStore.getAll(), settings); + //TODO: Update server. - //Facebook Flux: We do an optimistic update on the client already. AppDispatcher.dispatchViewAction({ - type: ActionTypes.UPDATE_SETTINGS, - settings: settings + type: ActionTypes.SETTINGS_STORE, + cmd: StoreCmds.UPDATE, + data: settings }); } }; @@ -253,68 +275,71 @@ var RequestUtils = _.extend(_MessageUtils, { }); var ResponseUtils = _.extend(_MessageUtils, {}); -function Store() { - this._views = []; +function ListStore() { + EventEmitter.call(this); this.reset(); } -_.extend(Store.prototype, { +_.extend(ListStore.prototype, EventEmitter.prototype, { add: function (elem) { if (elem.id in this._pos_map) { return; } - - this._pos_map[elem.id] = this._list.length; - this._list.push(elem); - for (var i = 0; i < this._views.length; i++) { - this._views[i].add(elem); - } + this._pos_map[elem.id] = this.list.length; + this.list.push(elem); + this.emit("add", elem); }, update: function (elem) { if (!(elem.id in this._pos_map)) { return; } - - this._list[this._pos_map[elem.id]] = elem; - for (var i = 0; i < this._views.length; i++) { - this._views[i].update(elem); - } + this.list[this._pos_map[elem.id]] = elem; + this.emit("update", elem); }, remove: function (elem_id) { if (!(elem.id in this._pos_map)) { return; } - - this._list.splice(this._pos_map[elem_id], 1); + this.list.splice(this._pos_map[elem_id], 1); this._build_map(); - for (var i = 0; i < this._views.length; i++) { - this._views[i].remove(elem_id); - } + this.emit("remove", elem_id); }, reset: function (elems) { - this._list = elems || []; + this.list = elems || []; this._build_map(); - for (var i = 0; i < this._views.length; i++) { - this._views[i].recalculate(this._list); - } + this.emit("recalculate", this.list); }, _build_map: function () { this._pos_map = {}; - for (var i = 0; i < this._list.length; i++) { - var elem = this._list[i]; + for (var i = 0; i < this.list.length; i++) { + var elem = this.list[i]; this._pos_map[elem.id] = i; } }, get: function (elem_id) { - return this._list[this._pos_map[elem_id]]; + return this.list[this._pos_map[elem_id]]; }, - index: function(elem_id) { + index: function (elem_id) { return this._pos_map[elem_id]; } }); -function LiveStore(type) { - Store.call(this); +function DictStore() { + EventEmitter.call(this); + this.reset(); +} +_.extend(DictStore.prototype, EventEmitter.prototype, { + update: function (dict) { + _.merge(this.dict, dict); + this.emit("recalculate", this.dict); + }, + reset: function (dict) { + this.dict = dict || {}; + this.emit("recalculate", this.dict); + } +}); + +function LiveStoreMixin(type) { this.type = type; this._updates_before_fetch = undefined; @@ -328,7 +353,7 @@ function LiveStore(type) { this.fetch(); } } -_.extend(LiveStore.prototype, Store.prototype, { +_.extend(LiveStoreMixin.prototype, { handle: function (event) { if (event.type === ActionTypes.CONNECTION_OPEN) { return this.fetch(); @@ -347,18 +372,28 @@ _.extend(LiveStore.prototype, Store.prototype, { close: function () { AppDispatcher.unregister(this.handle); }, - fetch: function () { + fetch: function (data) { console.log("fetch " + this.type); if (this._fetchxhr) { this._fetchxhr.abort(); } - this._fetchxhr = $.getJSON("/" + this.type, this.handle_fetch.bind(this)); - this._updates_before_fetch = []; // (JS: empty array is true) + this._updates_before_fetch = []; // (JS: empty array is true) + if (data) { + this.handle_fetch(data); + } else { + this._fetchxhr = $.getJSON("/" + this.type) + .done(function (message) { + this.handle_fetch(message.data); + }.bind(this)) + .fail(function () { + EventLogActions.add_event("Could not fetch " + this.type); + }.bind(this)); + } }, handle_fetch: function (data) { this._fetchxhr = false; console.log(this.type + " fetched.", this._updates_before_fetch); - this.reset(data.list); + this.reset(data); var updates = this._updates_before_fetch; this._updates_before_fetch = false; for (var i = 0; i < updates.length; i++) { @@ -367,15 +402,40 @@ _.extend(LiveStore.prototype, Store.prototype, { }, }); +function LiveListStore(type) { + ListStore.call(this); + LiveStoreMixin.call(this, type); +} +_.extend(LiveListStore.prototype, ListStore.prototype, LiveStoreMixin.prototype); + +function LiveDictStore(type) { + DictStore.call(this); + LiveStoreMixin.call(this, type); +} +_.extend(LiveDictStore.prototype, DictStore.prototype, LiveStoreMixin.prototype); + function FlowStore() { - return new LiveStore(ActionTypes.FLOW_STORE); + return new LiveListStore(ActionTypes.FLOW_STORE); } +function SettingsStore() { + return new LiveDictStore(ActionTypes.SETTINGS_STORE); +} function EventLogStore() { - return new LiveStore(ActionTypes.EVENT_STORE); + LiveListStore.call(this, ActionTypes.EVENT_STORE); } +_.extend(EventLogStore.prototype, LiveListStore.prototype, { + fetch: function(){ + LiveListStore.prototype.fetch.apply(this, arguments); + if(this._fetchxhr){ + this._fetchxhr.fail(function(){ + this.handle_fetch(null); + }.bind(this)); + } + } +}); function SortByStoreOrder(elem) { return this.store.index(elem.id); } @@ -391,13 +451,25 @@ function StoreView(store, filt, sortfun) { sortfun = sortfun || default_sort; this.store = store; - this.store._views.push(this); - this.recalculate(this.store._list, filt, sortfun); + + this.add = this.add.bind(this); + this.update = this.update.bind(this); + this.remove = this.remove.bind(this); + this.recalculate = this.recalculate.bind(this); + this.store.addListener("add", this.add); + this.store.addListener("update", this.update); + this.store.addListener("remove", this.remove); + this.store.addListener("recalculate", this.recalculate); + + this.recalculate(this.store.list, filt, sortfun); } _.extend(StoreView.prototype, EventEmitter.prototype, { close: function () { - this.store._views = _.without(this.store._views, this); + this.store.removeListener("add", this.add); + this.store.removeListener("update", this.update); + this.store.removeListener("remove", this.remove); + this.store.removeListener("recalculate", this.recalculate); }, recalculate: function (elems, filt, sortfun) { if (filt) { @@ -463,34 +535,6 @@ _.extend(StoreView.prototype, EventEmitter.prototype, { } } }); -function _SettingsStore() { - EventEmitter.call(this); - - //FIXME: What do we do if we haven't requested anything from the server yet? - this.settings = { - version: "0.12", - showEventLog: true, - mode: "transparent", - }; -} -_.extend(_SettingsStore.prototype, EventEmitter.prototype, { - getAll: function () { - return this.settings; - }, - handle: function (action) { - switch (action.type) { - case ActionTypes.UPDATE_SETTINGS: - this.settings = action.settings; - this.emit("change"); - break; - default: - return; - } - } -}); - -var SettingsStore = new _SettingsStore(); -AppDispatcher.register(SettingsStore.handle.bind(SettingsStore)); function Connection(url) { @@ -1493,7 +1537,7 @@ var MainView = React.createClass({displayName: 'MainView', } }, selectFlowRelative: function (shift) { - var flows = this.state.view.flows; + var flows = this.state.view.list; var index; if (!this.getParams().flowId) { if (shift > 0) { @@ -1609,22 +1653,29 @@ var LogMessage = React.createClass({displayName: 'LogMessage', var EventLogContents = React.createClass({displayName: 'EventLogContents', mixins: [AutoScrollMixin, VirtualScrollMixin], - getInitialState: function(){ - var store = new EventLogStore(); - var view = new StoreView(store, function(entry){ - return this.props.filter[entry.level]; - }.bind(this)); - view.addListener("add recalculate", this.onEventLogChange); + getInitialState: function () { return { - store: store, - view: view, log: [] }; }, + componentWillMount: function () { + this.openView(this.props.eventStore); + }, componentWillUnmount: function () { - this.state.view.removeListener("add recalculate", this.onEventLogChange); + this.closeView(); + }, + openView: function (store) { + var view = new StoreView(store, function (entry) { + return this.props.filter[entry.level]; + }.bind(this)); + this.setState({ + view: view + }); + + view.addListener("add recalculate", this.onEventLogChange); + }, + closeView: function () { this.state.view.close(); - this.state.store.close(); }, onEventLogChange: function () { this.setState({ @@ -1632,9 +1683,13 @@ var EventLogContents = React.createClass({displayName: 'EventLogContents', }); }, componentWillReceiveProps: function (nextProps) { - if(nextProps.filter !== this.props.filter){ + if (nextProps.filter !== this.props.filter) { this.props.filter = nextProps.filter; // Dirty: Make sure that view filter sees the update. - this.state.view.recalculate(this.state.store._list); + this.state.view.recalculate(this.props.eventStore.list); + } + if (nextProps.eventStore !== this.props.eventStore) { + this.closeView(); + this.openView(nextProps.eventStore); } }, getDefaultProps: function () { @@ -1644,7 +1699,7 @@ var EventLogContents = React.createClass({displayName: 'EventLogContents', placeholderTagName: "div" }; }, - renderRow: function(elem){ + renderRow: function (elem) { return React.createElement(LogMessage, {key: elem.id, entry: elem}); }, render: function () { @@ -1714,7 +1769,7 @@ var EventLog = React.createClass({displayName: 'EventLog', ) ), - React.createElement(EventLogContents, {filter: this.state.filter}) + React.createElement(EventLogContents, {filter: this.state.filter, eventStore: this.props.eventStore}) ) ); } @@ -1740,27 +1795,36 @@ var Reports = React.createClass({displayName: 'Reports', var ProxyAppMain = React.createClass({displayName: 'ProxyAppMain', getInitialState: function () { + var eventStore = new EventLogStore(); + var flowStore = new FlowStore(); + var settings = new SettingsStore(); + _.extend(settings.dict,{ + showEventLog: true + }); return { - settings: SettingsStore.getAll(), - flowStore: new FlowStore() + settings: settings, + flowStore: flowStore, + eventStore: eventStore }; }, componentDidMount: function () { - SettingsStore.addListener("change", this.onSettingsChange); + this.state.settings.addListener("recalculate", this.onSettingsChange); }, componentWillUnmount: function () { - SettingsStore.removeListener("change", this.onSettingsChange); + this.state.settings.removeListener("recalculate", this.onSettingsChange); }, - onSettingsChange: function () { - this.setState({settings: SettingsStore.getAll()}); + onSettingsChange: function(){ + this.setState({ + settings: this.state.settings + }); }, render: function () { var eventlog; - if (this.state.settings.showEventLog) { + if (this.state.settings.dict.showEventLog) { eventlog = [ React.createElement(Splitter, {key: "splitter", axis: "y"}), - React.createElement(EventLog, {key: "eventlog"}) + React.createElement(EventLog, {key: "eventlog", eventStore: this.state.eventStore}) ]; } else { eventlog = null; @@ -1768,10 +1832,10 @@ var ProxyAppMain = React.createClass({displayName: 'ProxyAppMain', return ( React.createElement("div", {id: "container"}, - React.createElement(Header, {settings: this.state.settings}), - React.createElement(RouteHandler, {settings: this.state.settings, flowStore: this.state.flowStore}), + React.createElement(Header, {settings: this.state.settings.dict}), + React.createElement(RouteHandler, {settings: this.state.settings.dict, flowStore: this.state.flowStore}), eventlog, - React.createElement(Footer, {settings: this.state.settings}) + React.createElement(Footer, {settings: this.state.settings.dict}) ) ); } -- cgit v1.2.3 From 93e928dec4d1004d4f983ff343569f6966db9675 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 10 Dec 2014 17:44:45 +0100 Subject: web: add file menu stub --- libmproxy/web/static/js/app.js | 127 +++++++++++++++++++++++++++++------------ 1 file changed, 90 insertions(+), 37 deletions(-) (limited to 'libmproxy/web/static/js/app.js') diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index 13b57689..d90610de 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -72,28 +72,6 @@ var formatTimeStamp = function (seconds) { }; -function listToDict(list, propKey, propVal) { - var dict = {}; - for (var i = 0; i < list.length; i++) { - var e = list[i]; - dict[e[propKey]] = e[propVal]; - } - return dict; -} -function dictToList(dict, propKey, propVal) { - var list = []; - for (var key in dict) { - if (dict.hasOwnProperty(key)) { - var elem = {}; - elem[propKey] = key; - elem[propVal] = dict[key]; - list.push(elem); - } - } - return list; -} - - function EventEmitter() { this.listeners = {}; } @@ -165,14 +143,9 @@ var ActionTypes = { CONNECTION_CLOSE: "connection_close", CONNECTION_ERROR: "connection_error", - // Settings + // Stores SETTINGS_STORE: "settings", - UPDATE_SETTINGS: "update_settings", - - // EventLog EVENT_STORE: "events", - - // Flow FLOW_STORE: "flows", }; @@ -206,6 +179,7 @@ var SettingsActions = { //TODO: Update server. + //Facebook Flux: We do an optimistic update on the client already. AppDispatcher.dispatchViewAction({ type: ActionTypes.SETTINGS_STORE, cmd: StoreCmds.UPDATE, @@ -429,6 +403,9 @@ function EventLogStore() { _.extend(EventLogStore.prototype, LiveListStore.prototype, { fetch: function(){ LiveListStore.prototype.fetch.apply(this, arguments); + + // Make sure to display updates even if fetching all events failed. + // This way, we can send "fetch failed" log messages to the log. if(this._fetchxhr){ this._fetchxhr.fail(function(){ this.handle_fetch(null); @@ -774,17 +751,20 @@ var MainMenu = React.createClass({displayName: 'MainMenu', showEventLog: !this.props.settings.showEventLog }); }, - clearFlows: function(){ + clearFlows: function () { $.post("/flows/clear"); }, render: function () { return ( React.createElement("div", null, React.createElement("button", {className: "btn " + (this.props.settings.showEventLog ? "btn-primary" : "btn-default"), onClick: this.toggleEventLog}, - React.createElement("i", {className: "fa fa-database"}), " Display Event Log" - ), " ", + React.createElement("i", {className: "fa fa-database"}), + " Display Event Log" + ), + " ", React.createElement("button", {className: "btn btn-default", onClick: this.clearFlows}, - React.createElement("i", {className: "fa fa-eraser"}), " Clear Flows" + React.createElement("i", {className: "fa fa-eraser"}), + " Clear Flows" ) ) ); @@ -813,6 +793,80 @@ var ReportsMenu = React.createClass({displayName: 'ReportsMenu', } }); +var FileMenu = React.createClass({displayName: 'FileMenu', + getInitialState: function () { + return { + showFileMenu: false + }; + }, + handleFileClick: function (e) { + e.preventDefault(); + if (!this.state.showFileMenu) { + var close = function () { + this.setState({showFileMenu: false}); + document.removeEventListener("click", close); + }.bind(this); + document.addEventListener("click", close); + + this.setState({ + showFileMenu: true + }); + } + }, + handleNewClick: function(e){ + e.preventDefault(); + console.error("unimplemented: handleNewClick"); + }, + handleOpenClick: function(e){ + e.preventDefault(); + console.error("unimplemented: handleOpenClick"); + }, + handleSaveClick: function(e){ + e.preventDefault(); + console.error("unimplemented: handleSaveClick"); + }, + handleShutdownClick: function(e){ + e.preventDefault(); + console.error("unimplemented: handleShutdownClick"); + }, + render: function () { + var fileMenuClass = "dropdown pull-left" + (this.state.showFileMenu ? " open" : ""); + + return ( + React.createElement("div", {className: fileMenuClass}, + React.createElement("a", {href: "#", className: "special", onClick: this.handleFileClick}, " File "), + React.createElement("ul", {className: "dropdown-menu", role: "menu"}, + React.createElement("li", null, + React.createElement("a", {href: "#", onClick: this.handleNewClick}, + React.createElement("i", {className: "fa fa-fw fa-file"}), + "New" + ) + ), + React.createElement("li", null, + React.createElement("a", {href: "#", onClick: this.handleOpenClick}, + React.createElement("i", {className: "fa fa-fw fa-folder-open"}), + "Open" + ) + ), + React.createElement("li", null, + React.createElement("a", {href: "#", onClick: this.handleSaveClick}, + React.createElement("i", {className: "fa fa-fw fa-save"}), + "Save" + ) + ), + React.createElement("li", {role: "presentation", className: "divider"}), + React.createElement("li", null, + React.createElement("a", {href: "#", onClick: this.handleShutdownClick}, + React.createElement("i", {className: "fa fa-fw fa-plug"}), + "Shutdown" + ) + ) + ) + ) + ); + } +}); + var header_entries = [MainMenu, ToolsMenu, ReportsMenu]; @@ -829,9 +883,6 @@ var Header = React.createClass({displayName: 'Header', this.transitionTo(active.route); this.setState({active: active}); }, - handleFileClick: function () { - console.log("File click"); - }, render: function () { var header = header_entries.map(function (entry, i) { var classes = React.addons.classSet({ @@ -851,10 +902,10 @@ var Header = React.createClass({displayName: 'Header', return ( React.createElement("header", null, React.createElement("div", {className: "title-bar"}, - "mitmproxy ", this.props.settings.version + "mitmproxy ", this.props.settings.version ), React.createElement("nav", {className: "nav-tabs nav-tabs-lg"}, - React.createElement("a", {href: "#", className: "special", onClick: this.handleFileClick}, " File "), + React.createElement(FileMenu, null), header ), React.createElement("div", {className: "menu"}, @@ -1798,6 +1849,8 @@ var ProxyAppMain = React.createClass({displayName: 'ProxyAppMain', var eventStore = new EventLogStore(); var flowStore = new FlowStore(); var settings = new SettingsStore(); + + // Default Settings before fetch _.extend(settings.dict,{ showEventLog: true }); -- cgit v1.2.3