From 7ca1ac0f3b7856c0ae44bfbf3b27ae4a424a1cc2 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 28 Nov 2014 16:03:56 +0100 Subject: [PATCH] web: virtual scrolling --- libmproxy/web/static/js/app.js | 143 ++++++++++++++++--------- web/src/js/components/flowtable.jsx.js | 107 +++++++++++++----- web/src/js/components/mainview.jsx.js | 14 +-- web/src/js/stores/base.js | 22 ++-- 4 files changed, 190 insertions(+), 96 deletions(-) diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index 78108b38b..dd1259f4c 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, diff --git a/web/src/js/components/flowtable.jsx.js b/web/src/js/components/flowtable.jsx.js index 6b56e5121..76ceea41f 100644 --- a/web/src/js/components/flowtable.jsx.js +++ b/web/src/js/components/flowtable.jsx.js @@ -14,11 +14,13 @@ var FlowRow = React.createClass({ ); }, 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) + //); } }); @@ -33,30 +35,51 @@ var FlowTableHead = React.createClass({ } }); -var FlowTableBody = React.createClass({ - render: function () { - var rows = this.props.flows.map(function (flow) { - var selected = (flow == this.props.selected); - return ; - }.bind(this)); - return {rows}; - } -}); +var ROW_HEIGHT = 32; var FlowTable = React.createClass({ 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(); @@ -77,16 +100,46 @@ var FlowTable = React.createClass({ } }, 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( + + ); + } + + 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 = ; + } + } + + return ( -
+
- + + + { fix_nth_row } + {rows} + +
); diff --git a/web/src/js/components/mainview.jsx.js b/web/src/js/components/mainview.jsx.js index a1c9772e2..fd9fdb8dc 100644 --- a/web/src/js/components/mainview.jsx.js +++ b/web/src/js/components/mainview.jsx.js @@ -16,26 +16,16 @@ var MainView = React.createClass({ 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( @@ -131,7 +121,7 @@ var MainView = React.createClass({ return (
{ details ? : null } diff --git a/web/src/js/stores/base.js b/web/src/js/stores/base.js index 952fa8474..cf9f015e2 100644 --- a/web/src/js/stores/base.js +++ b/web/src/js/stores/base.js @@ -10,16 +10,20 @@ 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)); };