aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/js')
-rw-r--r--web/src/js/components/common.js65
-rw-r--r--web/src/js/components/editor.js7
-rw-r--r--web/src/js/components/eventlog.js231
-rw-r--r--web/src/js/components/flowtable.js340
-rw-r--r--web/src/js/components/footer.js31
-rw-r--r--web/src/js/components/header.js20
-rw-r--r--web/src/js/components/helpers/AutoScroll.js25
-rw-r--r--web/src/js/components/helpers/VirtualScroll.js70
-rw-r--r--web/src/js/components/mainview.js14
-rw-r--r--web/src/js/components/prompt.js7
-rw-r--r--web/src/js/components/proxyapp.js18
-rw-r--r--web/src/js/components/virtualscroll.js84
12 files changed, 470 insertions, 442 deletions
diff --git a/web/src/js/components/common.js b/web/src/js/components/common.js
index 5fae7415..ad97ab38 100644
--- a/web/src/js/components/common.js
+++ b/web/src/js/components/common.js
@@ -2,68 +2,6 @@ import React from "react"
import ReactDOM from "react-dom"
import _ from "lodash"
-// http://blog.vjeux.com/2013/javascript/scroll-position-with-react.html (also contains inverse example)
-export var AutoScrollMixin = {
- componentWillUpdate: function () {
- var node = ReactDOM.findDOMNode(this);
- this._shouldScrollBottom = (
- node.scrollTop !== 0 &&
- node.scrollTop + node.clientHeight === node.scrollHeight
- );
- },
- componentDidUpdate: function () {
- if (this._shouldScrollBottom) {
- var node = ReactDOM.findDOMNode(this);
- node.scrollTop = node.scrollHeight;
- }
- }
-};
-
-
-export var StickyHeadMixin = {
- adjustHead: function () {
- // Abusing CSS transforms to set the element
- // referenced as head into some kind of position:sticky.
- var head = ReactDOM.findDOMNode(this.refs.head);
- head.style.transform = "translate(0," + ReactDOM.findDOMNode(this).scrollTop + "px)";
- }
-};
-
-export var SettingsState = {
- contextTypes: {
- settingsStore: React.PropTypes.object.isRequired
- },
- getInitialState: function () {
- return {
- settings: this.context.settingsStore.dict
- };
- },
- componentDidMount: function () {
- this.context.settingsStore.addListener("recalculate", this.onSettingsChange);
- },
- componentWillUnmount: function () {
- this.context.settingsStore.removeListener("recalculate", this.onSettingsChange);
- },
- onSettingsChange: function () {
- this.setState({
- settings: this.context.settingsStore.dict
- });
- },
-};
-
-
-export var ChildFocus = {
- contextTypes: {
- returnFocus: React.PropTypes.func
- },
- returnFocus: function () {
- ReactDOM.findDOMNode(this).blur();
- window.getSelection().removeAllRanges();
- this.context.returnFocus();
- }
-};
-
-
export var Router = {
contextTypes: {
location: React.PropTypes.object,
@@ -87,9 +25,6 @@ export var Router = {
// For whatever reason, react-router always returns the same object, which makes comparing
// the current props with nextProps impossible. As a workaround, we just clone the query object.
return _.clone(this.context.location.query);
- },
- getParams: function() {
- return this.props.routeParams;
}
};
diff --git a/web/src/js/components/editor.js b/web/src/js/components/editor.js
index c929a244..eed2f7c6 100644
--- a/web/src/js/components/editor.js
+++ b/web/src/js/components/editor.js
@@ -1,6 +1,5 @@
import React from "react";
import ReactDOM from 'react-dom';
-import {ChildFocus} from "./common.js";
import {Key} from "../utils.js";
var contentToHtml = function (content) {
@@ -214,7 +213,9 @@ var ValidateEditor = React.createClass({
Text Editor with mitmweb-specific convenience features
*/
export var ValueEditor = React.createClass({
- mixins: [ChildFocus],
+ contextTypes: {
+ returnFocus: React.PropTypes.func
+ },
propTypes: {
content: React.PropTypes.string.isRequired,
onDone: React.PropTypes.func.isRequired,
@@ -232,6 +233,6 @@ export var ValueEditor = React.createClass({
ReactDOM.findDOMNode(this).focus();
},
onStop: function () {
- this.returnFocus();
+ this.context.returnFocus();
}
}); \ No newline at end of file
diff --git a/web/src/js/components/eventlog.js b/web/src/js/components/eventlog.js
index 650f7cd3..d1b23ace 100644
--- a/web/src/js/components/eventlog.js
+++ b/web/src/js/components/eventlog.js
@@ -1,115 +1,151 @@
import React from "react"
-import {AutoScrollMixin, Router} from "./common.js"
+import ReactDOM from "react-dom"
+import shallowEqual from "shallowequal"
+import {Router} from "./common.js"
import {Query} from "../actions.js"
-import { VirtualScrollMixin } from "./virtualscroll.js"
+import AutoScroll from "./helpers/AutoScroll";
+import {calcVScroll} from "./helpers/VirtualScroll"
import {StoreView} from "../store/view.js"
import _ from "lodash"
-var LogMessage = React.createClass({
- render: function () {
- var entry = this.props.entry;
- var indicator;
- switch (entry.level) {
- case "web":
- indicator = <i className="fa fa-fw fa-html5"></i>;
- break;
- case "debug":
- indicator = <i className="fa fa-fw fa-bug"></i>;
- break;
- default:
- indicator = <i className="fa fa-fw fa-info"></i>;
- }
- return (
- <div>
- { indicator } {entry.message}
- </div>
+class EventLogContents extends React.Component {
+
+ static contextTypes = {
+ eventStore: React.PropTypes.object.isRequired,
+ };
+
+ static defaultProps = {
+ rowHeight: 18,
+ };
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.view = new StoreView(
+ this.context.eventStore,
+ entry => this.props.filter[entry.level]
);
- },
- shouldComponentUpdate: function () {
- return false; // log entries are immutable.
+
+ this.heights = {};
+ this.state = { entries: this.view.list, vScroll: calcVScroll() };
+
+ this.onChange = this.onChange.bind(this);
+ this.onViewportUpdate = this.onViewportUpdate.bind(this);
}
-});
-var EventLogContents = React.createClass({
- contextTypes: {
- eventStore: React.PropTypes.object.isRequired
- },
- mixins: [AutoScrollMixin, VirtualScrollMixin],
- getInitialState: function () {
- var filterFn = function (entry) {
- return this.props.filter[entry.level];
- };
- var view = new StoreView(this.context.eventStore, filterFn.bind(this));
- view.addListener("add", this.onEventLogChange);
- view.addListener("recalculate", this.onEventLogChange);
+ componentDidMount() {
+ window.addEventListener("resize", this.onViewportUpdate);
+ this.view.addListener("add", this.onChange);
+ this.view.addListener("recalculate", this.onChange);
+ this.onViewportUpdate();
+ }
- return {
- view: view
- };
- },
- componentWillUnmount: function () {
- this.state.view.close();
- },
- filter: function (entry) {
- return this.props.filter[entry.level];
- },
- onEventLogChange: function () {
- this.forceUpdate();
- },
- componentWillReceiveProps: function (nextProps) {
+ componentWillUnmount() {
+ window.removeEventListener("resize", this.onViewportUpdate);
+ this.view.removeListener("add", this.onChange);
+ this.view.removeListener("recalculate", this.onChange);
+ this.view.close();
+ }
+
+ componentDidUpdate() {
+ this.onViewportUpdate();
+ }
+
+ componentWillReceiveProps(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.view.recalculate(
+ entry => nextProps.filter[entry.level]
+ );
}
- },
- getDefaultProps: function () {
- return {
- rowHeight: 45,
- rowHeightMin: 15,
- placeholderTagName: "div"
- };
- },
- renderRow: function (elem) {
- return <LogMessage key={elem.id} entry={elem}/>;
- },
- render: function () {
- var entries = this.state.view.list;
- var rows = this.renderRows(entries);
-
- return <pre onScroll={this.onScroll}>
- { this.getPlaceholderTop(entries.length) }
- {rows}
- { this.getPlaceholderBottom(entries.length) }
- </pre>;
}
-});
-var ToggleFilter = React.createClass({
- toggle: function (e) {
- e.preventDefault();
- return this.props.toggleLevel(this.props.name);
- },
- render: function () {
- var className = "label ";
- if (this.props.active) {
- className += "label-primary";
- } else {
- className += "label-default";
+ onViewportUpdate() {
+ const viewport = ReactDOM.findDOMNode(this);
+
+ const vScroll = calcVScroll({
+ itemCount: this.state.entries.length,
+ rowHeight: this.props.rowHeight,
+ viewportTop: viewport.scrollTop,
+ viewportHeight: viewport.offsetHeight,
+ itemHeights: this.state.entries.map(entry => this.heights[entry.id]),
+ });
+
+ if (!shallowEqual(this.state.vScroll, vScroll)) {
+ this.setState({ vScroll });
+ }
+ }
+
+ onChange() {
+ this.setState({ entries: this.view.list });
+ }
+
+ setHeight(id, ref) {
+ if (ref && !this.heights[id]) {
+ const height = ReactDOM.findDOMNode(ref).offsetHeight;
+ if (this.heights[id] !== height) {
+ this.heights[id] = height;
+ this.onViewportUpdate();
+ }
}
+ }
+
+ getIcon(level) {
+ return { web: "html5", debug: "bug" }[level] || "info";
+ }
+
+ render() {
+ const vScroll = this.state.vScroll;
+ const entries = this.state.entries.slice(vScroll.start, vScroll.end);
+
return (
- <a
- href="#"
- className={className}
- onClick={this.toggle}>
- {this.props.name}
- </a>
+ <pre onScroll={this.onViewportUpdate}>
+ <div style={{ height: vScroll.paddingTop }}></div>
+ {entries.map((entry, index) => (
+ <div key={entry.id} ref={this.setHeight.bind(this, entry.id)}>
+ <i className={`fa fa-fw fa-${this.getIcon(entry.level)}`}></i>
+ {entry.message}
+ </div>
+ ))}
+ <div style={{ height: vScroll.paddingBottom }}></div>
+ </pre>
);
}
-});
+}
+
+ToggleFilter.propTypes = {
+ name: React.PropTypes.string.isRequired,
+ toggleLevel: React.PropTypes.func.isRequired,
+ active: React.PropTypes.bool,
+};
+
+function ToggleFilter ({ name, active, toggleLevel }) {
+ let className = "label ";
+ if (active) {
+ className += "label-primary";
+ } else {
+ className += "label-default";
+ }
+
+ function onClick(event) {
+ event.preventDefault();
+ toggleLevel(name);
+ }
+
+ return (
+ <a
+ href="#"
+ className={className}
+ onClick={onClick}>
+ {name}
+ </a>
+ );
+}
+
+const AutoScrollEventLog = AutoScroll(EventLogContents);
var EventLog = React.createClass({
mixins: [Router],
- getInitialState: function () {
+ getInitialState() {
return {
filter: {
"debug": false,
@@ -118,18 +154,17 @@ var EventLog = React.createClass({
}
};
},
- close: function () {
+ close() {
var d = {};
d[Query.SHOW_EVENTLOG] = undefined;
-
this.updateLocation(undefined, d);
},
- toggleLevel: function (level) {
+ toggleLevel(level) {
var filter = _.extend({}, this.state.filter);
filter[level] = !filter[level];
this.setState({filter: filter});
},
- render: function () {
+ render() {
return (
<div className="eventlog">
<div>
@@ -142,10 +177,10 @@ var EventLog = React.createClass({
</div>
</div>
- <EventLogContents filter={this.state.filter}/>
+ <AutoScrollEventLog filter={this.state.filter}/>
</div>
);
}
});
-export default EventLog; \ No newline at end of file
+export default EventLog;
diff --git a/web/src/js/components/flowtable.js b/web/src/js/components/flowtable.js
index 988d1895..f03b8ec0 100644
--- a/web/src/js/components/flowtable.js
+++ b/web/src/js/components/flowtable.js
@@ -1,188 +1,216 @@
import React from "react";
-import ReactDOM from 'react-dom';
-import {StickyHeadMixin, AutoScrollMixin} from "./common.js";
+import ReactDOM from "react-dom";
+import classNames from "classnames";
import {reverseString} from "../utils.js";
import _ from "lodash";
-
-import { VirtualScrollMixin } from "./virtualscroll.js"
+import shallowEqual from "shallowequal";
+import AutoScroll from "./helpers/AutoScroll";
+import {calcVScroll} from "./helpers/VirtualScroll";
import flowtable_columns from "./flowtable-columns.js";
-var FlowRow = React.createClass({
- render: function () {
- var flow = this.props.flow;
- var columns = this.props.columns.map(function (Column) {
- return <Column key={Column.displayName} flow={flow}/>;
- }.bind(this));
- var className = "";
- if (this.props.selected) {
- className += " selected";
- }
- if (this.props.highlighted) {
- className += " highlighted";
- }
- if (flow.intercepted) {
- className += " intercepted";
- }
- if (flow.request) {
- className += " has-request";
- }
- if (flow.response) {
- className += " has-response";
- }
+FlowRow.propTypes = {
+ selectFlow: React.PropTypes.func.isRequired,
+ columns: React.PropTypes.array.isRequired,
+ flow: React.PropTypes.object.isRequired,
+ highlighted: React.PropTypes.bool,
+ selected: React.PropTypes.bool,
+};
- return (
- <tr className={className} onClick={this.props.selectFlow.bind(null, flow)}>
- {columns}
- </tr>);
- },
- shouldComponentUpdate: function (nextProps) {
- 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)
- //);
+function FlowRow(props) {
+ const flow = props.flow;
+
+ const className = classNames({
+ "selected": props.selected,
+ "highlighted": props.highlighted,
+ "intercepted": flow.intercepted,
+ "has-request": flow.request,
+ "has-response": flow.response,
+ });
+
+ return (
+ <tr className={className} onClick={() => props.selectFlow(flow)}>
+ {props.columns.map(Column => (
+ <Column key={Column.displayName} flow={flow}/>
+ ))}
+ </tr>
+ );
+}
+
+class FlowTableHead extends React.Component {
+
+ static propTypes = {
+ setSortKeyFun: React.PropTypes.func.isRequired,
+ columns: React.PropTypes.array.isRequired,
+ };
+
+ constructor(props, context) {
+ super(props, context);
+ this.state = { sortColumn: undefined, sortDesc: false };
}
-});
-
-var FlowTableHead = React.createClass({
- getInitialState: function(){
- return {
- sortColumn: undefined,
- sortDesc: false
- };
- },
- onClick: function(Column){
- var sortDesc = this.state.sortDesc;
- var hasSort = Column.sortKeyFun;
- if(Column === this.state.sortColumn){
+
+ onClick(Column) {
+ const hasSort = Column.sortKeyFun;
+
+ let sortDesc = this.state.sortDesc;
+
+ if (Column === this.state.sortColumn) {
sortDesc = !sortDesc;
- this.setState({
- sortDesc: sortDesc
- });
+ this.setState({ sortDesc });
} else {
- this.setState({
- sortColumn: hasSort && Column,
- sortDesc: false
- })
+ this.setState({ sortColumn: hasSort && Column, sortDesc: false });
}
- var sortKeyFun;
- if(!sortDesc){
- sortKeyFun = Column.sortKeyFun;
- } else {
- sortKeyFun = hasSort && function(){
- var k = Column.sortKeyFun.apply(this, arguments);
- if(_.isString(k)){
- return reverseString(""+k);
- } else {
- return -k;
+
+ let sortKeyFun = Column.sortKeyFun;
+ if (sortDesc) {
+ sortKeyFun = hasSort && function() {
+ const k = Column.sortKeyFun.apply(this, arguments);
+ if (_.isString(k)) {
+ return reverseString("" + k);
}
- }
+ return -k;
+ };
}
+
this.props.setSortKeyFun(sortKeyFun);
- },
- render: function () {
- var columns = this.props.columns.map(function (Column) {
- var onClick = this.onClick.bind(this, Column);
- var className;
- if(this.state.sortColumn === Column) {
- if(this.state.sortDesc){
- className = "sort-desc";
- } else {
- className = "sort-asc";
- }
- }
- return <Column.Title
+ }
+
+ render() {
+ const sortColumn = this.state.sortColumn;
+ const sortType = this.state.sortDesc ? "sort-desc" : "sort-asc";
+ return (
+ <tr>
+ {this.props.columns.map(Column => (
+ <Column.Title
key={Column.displayName}
- onClick={onClick}
- className={className} />;
- }.bind(this));
- return <thead>
- <tr>{columns}</tr>
- </thead>;
+ onClick={() => this.onClick(Column)}
+ className={sortColumn === Column && sortType}
+ />
+ ))}
+ </tr>
+ );
+ }
+}
+
+class FlowTable extends React.Component {
+
+ static contextTypes = {
+ view: React.PropTypes.object.isRequired,
+ };
+
+ static propTypes = {
+ rowHeight: React.PropTypes.number,
+ };
+
+ static defaultProps = {
+ rowHeight: 32,
+ };
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = { flows: [], vScroll: calcVScroll() };
+
+ this.onChange = this.onChange.bind(this);
+ this.onViewportUpdate = this.onViewportUpdate.bind(this);
}
-});
-
-
-var ROW_HEIGHT = 32;
-
-var FlowTable = React.createClass({
- mixins: [StickyHeadMixin, AutoScrollMixin, VirtualScrollMixin],
- contextTypes: {
- view: React.PropTypes.object.isRequired
- },
- getInitialState: function () {
- return {
- columns: flowtable_columns
- };
- },
- componentWillMount: function () {
+
+ componentWillMount() {
+ window.addEventListener("resize", this.onViewportUpdate);
this.context.view.addListener("add", this.onChange);
this.context.view.addListener("update", this.onChange);
this.context.view.addListener("remove", this.onChange);
this.context.view.addListener("recalculate", this.onChange);
- },
- componentWillUnmount: function(){
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener("resize", this.onViewportUpdate);
this.context.view.removeListener("add", this.onChange);
this.context.view.removeListener("update", this.onChange);
this.context.view.removeListener("remove", this.onChange);
this.context.view.removeListener("recalculate", this.onChange);
- },
- getDefaultProps: function () {
- return {
- rowHeight: ROW_HEIGHT
- };
- },
- onScrollFlowTable: function () {
- this.adjustHead();
- this.onScroll();
- },
- onChange: function () {
- this.forceUpdate();
- },
- scrollIntoView: function (flow) {
- this.scrollRowIntoView(
- this.context.view.indexOf(flow),
- ReactDOM.findDOMNode(this.refs.body).offsetTop
- );
- },
- renderRow: function (flow) {
- var selected = (flow === this.props.selected);
- var highlighted =
- (
- this.context.view._highlight &&
- this.context.view._highlight[flow.id]
- );
-
- return <FlowRow key={flow.id}
- ref={flow.id}
- flow={flow}
- columns={this.state.columns}
- selected={selected}
- highlighted={highlighted}
- selectFlow={this.props.selectFlow}
- />;
- },
- render: function () {
- var flows = this.context.view.list;
- var rows = this.renderRows(flows);
+ }
+
+ componentDidUpdate() {
+ this.onViewportUpdate();
+ }
+
+ onViewportUpdate() {
+ const viewport = ReactDOM.findDOMNode(this);
+ const viewportTop = viewport.scrollTop;
+
+ const vScroll = calcVScroll({
+ viewportTop,
+ viewportHeight: viewport.offsetHeight,
+ itemCount: this.state.flows.length,
+ rowHeight: this.props.rowHeight,
+ });
+
+ if (!shallowEqual(this.state.vScroll, vScroll) ||
+ this.state.viewportTop !== viewportTop) {
+ this.setState({ vScroll, viewportTop });
+ }
+ }
+
+ onChange() {
+ this.setState({ flows: this.context.view.list });
+ }
+
+ scrollIntoView(flow) {
+ const viewport = ReactDOM.findDOMNode(this);
+ const index = this.context.view.indexOf(flow);
+ const rowHeight = this.props.rowHeight;
+ const head = ReactDOM.findDOMNode(this.refs.head);
+
+ const headHeight = head ? head.offsetHeight : 0;
+
+ const rowTop = (index * rowHeight) + headHeight;
+ const rowBottom = rowTop + rowHeight;
+
+ const viewportTop = viewport.scrollTop;
+ const viewportHeight = viewport.offsetHeight;
+
+ // Account for pinned thead
+ if (rowTop - headHeight < viewportTop) {
+ viewport.scrollTop = rowTop - headHeight;
+ } else if (rowBottom > viewportTop + viewportHeight) {
+ viewport.scrollTop = rowBottom - viewportHeight;
+ }
+ }
+
+ render() {
+ const vScroll = this.state.vScroll;
+ const highlight = this.context.view._highlight;
+ const flows = this.state.flows.slice(vScroll.start, vScroll.end);
+
+ const transform = `translate(0,${this.state.viewportTop}px)`;
return (
- <div className="flow-table" onScroll={this.onScrollFlowTable}>
+ <div className="flow-table" onScroll={this.onViewportUpdate}>
<table>
- <FlowTableHead ref="head"
- columns={this.state.columns}
- setSortKeyFun={this.props.setSortKeyFun}/>
- <tbody ref="body">
- { this.getPlaceholderTop(flows.length) }
- {rows}
- { this.getPlaceholderBottom(flows.length) }
+ <thead ref="head" style={{ transform }}>
+ <FlowTableHead
+ columns={flowtable_columns}
+ setSortKeyFun={this.props.setSortKeyFun}
+ />
+ </thead>
+ <tbody>
+ <tr style={{ height: vScroll.paddingTop }}></tr>
+ {flows.map(flow => (
+ <FlowRow
+ key={flow.id}
+ flow={flow}
+ columns={flowtable_columns}
+ selected={flow === this.props.selected}
+ highlighted={highlight && highlight[flow.id]}
+ selectFlow={this.props.selectFlow}
+ />
+ ))}
+ <tr style={{ height: vScroll.paddingBottom }}></tr>
</tbody>
</table>
</div>
);
}
-});
+}
-export default FlowTable;
+export default AutoScroll(FlowTable);
diff --git a/web/src/js/components/footer.js b/web/src/js/components/footer.js
index 415c2577..e2d96288 100644
--- a/web/src/js/components/footer.js
+++ b/web/src/js/components/footer.js
@@ -1,19 +1,20 @@
import React from "react";
import {SettingsState} from "./common.js";
-var Footer = React.createClass({
- mixins: [SettingsState],
- render: function () {
- var mode = this.state.settings.mode;
- var intercept = this.state.settings.intercept;
- return (
- <footer>
- {mode && mode != "regular" ? <span className="label label-success">{mode} mode</span> : null}
- &nbsp;
- {intercept ? <span className="label label-success">Intercept: {intercept}</span> : null}
- </footer>
- );
- }
-});
+Footer.propTypes = {
+ settings: React.PropTypes.object.isRequired,
+};
-export default Footer; \ No newline at end of file
+export default function Footer({ settings }) {
+ const {mode, intercept} = settings;
+ return (
+ <footer>
+ {mode && mode != "regular" && (
+ <span className="label label-success">{mode} mode</span>
+ )}
+ {intercept && (
+ <span className="label label-success">Intercept: {intercept}</span>
+ )}
+ </footer>
+ );
+}
diff --git a/web/src/js/components/header.js b/web/src/js/components/header.js
index 3833a6ee..1af928a3 100644
--- a/web/src/js/components/header.js
+++ b/web/src/js/components/header.js
@@ -4,7 +4,7 @@ import $ from "jquery";
import Filt from "../filt/filt.js";
import {Key} from "../utils.js";
-import {Router, SettingsState, ChildFocus} from "./common.js";
+import {Router} from "./common.js";
import {SettingsActions, FlowActions} from "../actions.js";
import {Query} from "../actions.js";
@@ -51,7 +51,9 @@ var FilterDocs = React.createClass({
}
});
var FilterInput = React.createClass({
- mixins: [ChildFocus],
+ contextTypes: {
+ returnFocus: React.PropTypes.func
+ },
getInitialState: function () {
// Consider both focus and mouseover for showing/hiding the tooltip,
// because onBlur of the input is triggered before the click on the tooltip
@@ -118,7 +120,7 @@ var FilterInput = React.createClass({
},
blur: function () {
ReactDOM.findDOMNode(this.refs.input).blur();
- this.returnFocus();
+ this.context.returnFocus();
},
select: function () {
ReactDOM.findDOMNode(this.refs.input).select();
@@ -159,7 +161,10 @@ var FilterInput = React.createClass({
});
export var MainMenu = React.createClass({
- mixins: [Router, SettingsState],
+ mixins: [Router],
+ propTypes: {
+ settings: React.PropTypes.object.isRequired,
+ },
statics: {
title: "Start",
route: "flows"
@@ -180,7 +185,7 @@ export var MainMenu = React.createClass({
render: function () {
var search = this.getQuery()[Query.SEARCH] || "";
var highlight = this.getQuery()[Query.HIGHLIGHT] || "";
- var intercept = this.state.settings.intercept || "";
+ var intercept = this.props.settings.intercept || "";
return (
<div>
@@ -349,6 +354,9 @@ var header_entries = [MainMenu, ViewMenu /*, ReportsMenu */];
export var Header = React.createClass({
mixins: [Router],
+ propTypes: {
+ settings: React.PropTypes.object.isRequired,
+ },
getInitialState: function () {
return {
active: header_entries[0]
@@ -384,7 +392,7 @@ export var Header = React.createClass({
{header}
</nav>
<div className="menu">
- <this.state.active ref="active"/>
+ <this.state.active ref="active" settings={this.props.settings}/>
</div>
</header>
);
diff --git a/web/src/js/components/helpers/AutoScroll.js b/web/src/js/components/helpers/AutoScroll.js
new file mode 100644
index 00000000..d37b9f37
--- /dev/null
+++ b/web/src/js/components/helpers/AutoScroll.js
@@ -0,0 +1,25 @@
+import React from "react";
+import ReactDOM from "react-dom";
+
+const symShouldStick = Symbol("shouldStick");
+const isAtBottom = v => v.scrollTop + v.clientHeight === v.scrollHeight;
+
+export default Component => Object.assign(class AutoScrollWrapper extends Component {
+
+ static displayName = Component.name;
+
+ componentWillUpdate() {
+ const viewport = ReactDOM.findDOMNode(this);
+ this[symShouldStick] = viewport.scrollTop && isAtBottom(viewport);
+ super.componentWillUpdate && super.componentWillUpdate();
+ }
+
+ componentDidUpdate() {
+ const viewport = ReactDOM.findDOMNode(this);
+ if (this[symShouldStick] && !isAtBottom(viewport)) {
+ viewport.scrollTop = viewport.scrollHeight;
+ }
+ super.componentDidUpdate && super.componentDidUpdate();
+ }
+
+}, Component);
diff --git a/web/src/js/components/helpers/VirtualScroll.js b/web/src/js/components/helpers/VirtualScroll.js
new file mode 100644
index 00000000..5d4cf796
--- /dev/null
+++ b/web/src/js/components/helpers/VirtualScroll.js
@@ -0,0 +1,70 @@
+/**
+ * Calculate virtual scroll stuffs
+ *
+ * @param {?Object} opts Options for calculation
+ *
+ * @returns {Object} result
+ *
+ * __opts__ should have following properties:
+ * - {number} itemCount
+ * - {number} rowHeight
+ * - {number} viewportTop
+ * - {number} viewportHeight
+ * - {Array<?number>} [itemHeights]
+ *
+ * __result__ have following properties:
+ * - {number} start
+ * - {number} end
+ * - {number} paddingTop
+ * - {number} paddingBottom
+ */
+export function calcVScroll(opts) {
+ if (!opts) {
+ return { start: 0, end: 0, paddingTop: 0, paddingBottom: 0 };
+ }
+
+ const { itemCount, rowHeight, viewportTop, viewportHeight, itemHeights } = opts;
+ const viewportBottom = viewportTop + viewportHeight;
+
+ let start = 0;
+ let end = 0;
+
+ let paddingTop = 0;
+ let paddingBottom = 0;
+
+ if (itemHeights) {
+
+ for (let i = 0, pos = 0; i < itemCount; i++) {
+ const height = itemHeights[i] || rowHeight;
+
+ if (pos <= viewportTop && i % 2 === 0) {
+ paddingTop = pos;
+ start = i;
+ }
+
+ if (pos <= viewportBottom) {
+ end = i + 1;
+ } else {
+ paddingBottom += height;
+ }
+
+ pos += height;
+ }
+
+ } else {
+
+ // Make sure that we start at an even row so that CSS `:nth-child(even)` is preserved
+ start = Math.max(0, Math.floor(viewportTop / rowHeight) - 1) & ~1;
+ end = Math.min(
+ itemCount,
+ start + Math.ceil(viewportHeight / rowHeight) + 1
+ );
+
+ // 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.
+ paddingTop = Math.min(start, itemCount) * rowHeight;
+ paddingBottom = Math.max(0, itemCount - end) * rowHeight;
+ }
+
+ return { start, end, paddingTop, paddingBottom };
+}
diff --git a/web/src/js/components/mainview.js b/web/src/js/components/mainview.js
index cc1f05ae..87c0c4bd 100644
--- a/web/src/js/components/mainview.js
+++ b/web/src/js/components/mainview.js
@@ -75,12 +75,12 @@ var MainView = React.createClass({
}
},
onUpdate: function (flow) {
- if (flow.id === this.getParams().flowId) {
+ if (flow.id === this.props.routeParams.flowId) {
this.forceUpdate();
}
},
onRemove: function (flow_id, index) {
- if (flow_id === this.getParams().flowId) {
+ if (flow_id === this.props.routeParams.flowId) {
var flow_to_select = this.state.view.list[Math.min(index, this.state.view.list.length - 1)];
this.selectFlow(flow_to_select);
}
@@ -93,7 +93,7 @@ var MainView = React.createClass({
},
selectFlow: function (flow) {
if (flow) {
- var tab = this.getParams().detailTab || "request";
+ var tab = this.props.routeParams.detailTab || "request";
this.updateLocation(`/flows/${flow.id}/${tab}`);
this.refs.flowTable.scrollIntoView(flow);
} else {
@@ -103,14 +103,14 @@ var MainView = React.createClass({
selectFlowRelative: function (shift) {
var flows = this.state.view.list;
var index;
- if (!this.getParams().flowId) {
+ if (!this.props.routeParams.flowId) {
if (shift < 0) {
index = flows.length - 1;
} else {
index = 0;
}
} else {
- var currFlowId = this.getParams().flowId;
+ var currFlowId = this.props.routeParams.flowId;
var i = flows.length;
while (i--) {
if (flows[i].id === currFlowId) {
@@ -212,7 +212,7 @@ var MainView = React.createClass({
e.preventDefault();
},
getSelected: function () {
- return this.context.flowStore.get(this.getParams().flowId);
+ return this.context.flowStore.get(this.props.routeParams.flowId);
},
render: function () {
var selected = this.getSelected();
@@ -224,7 +224,7 @@ var MainView = React.createClass({
<FlowView
key="flowDetails"
ref="flowDetails"
- tab={this.getParams().detailTab}
+ tab={this.props.routeParams.detailTab}
flow={selected}/>
];
} else {
diff --git a/web/src/js/components/prompt.js b/web/src/js/components/prompt.js
index 7b398038..e324f7d4 100644
--- a/web/src/js/components/prompt.js
+++ b/web/src/js/components/prompt.js
@@ -3,10 +3,11 @@ import ReactDOM from 'react-dom';
import _ from "lodash";
import {Key} from "../utils.js";
-import {ChildFocus} from "./common.js"
var Prompt = React.createClass({
- mixins: [ChildFocus],
+ contextTypes: {
+ returnFocus: React.PropTypes.func
+ },
propTypes: {
options: React.PropTypes.array.isRequired,
done: React.PropTypes.func.isRequired,
@@ -35,7 +36,7 @@ var Prompt = React.createClass({
},
done: function (ret) {
this.props.done(ret);
- this.returnFocus();
+ this.context.returnFocus();
},
getOptions: function () {
var opts = [];
diff --git a/web/src/js/components/proxyapp.js b/web/src/js/components/proxyapp.js
index 24f45ff5..d17a1522 100644
--- a/web/src/js/components/proxyapp.js
+++ b/web/src/js/components/proxyapp.js
@@ -23,7 +23,6 @@ var Reports = React.createClass({
var ProxyAppMain = React.createClass({
mixins: [Router],
childContextTypes: {
- settingsStore: React.PropTypes.object.isRequired,
flowStore: React.PropTypes.object.isRequired,
eventStore: React.PropTypes.object.isRequired,
returnFocus: React.PropTypes.func.isRequired,
@@ -31,10 +30,16 @@ var ProxyAppMain = React.createClass({
},
componentDidMount: function () {
this.focus();
+ this.settingsStore.addListener("recalculate", this.onSettingsChange);
+ },
+ componentWillUnmount: function () {
+ this.settingsStore.removeListener("recalculate", this.onSettingsChange);
+ },
+ onSettingsChange: function () {
+ this.setState({ settings: this.settingsStore.dict });
},
getChildContext: function () {
return {
- settingsStore: this.state.settingsStore,
flowStore: this.state.flowStore,
eventStore: this.state.eventStore,
returnFocus: this.focus,
@@ -46,15 +51,18 @@ var ProxyAppMain = React.createClass({
var flowStore = new FlowStore();
var settingsStore = new SettingsStore();
+ this.settingsStore = settingsStore;
// Default Settings before fetch
_.extend(settingsStore.dict, {});
return {
- settingsStore: settingsStore,
+ settings: settingsStore.dict,
flowStore: flowStore,
eventStore: eventStore
};
},
focus: function () {
+ document.activeElement.blur();
+ window.getSelection().removeAllRanges();
ReactDOM.findDOMNode(this).focus();
},
getMainComponent: function () {
@@ -104,10 +112,10 @@ var ProxyAppMain = React.createClass({
);
return (
<div id="container" tabIndex="0" onKeyDown={this.onKeydown}>
- <Header ref="header"/>
+ <Header ref="header" settings={this.state.settings}/>
{children}
{eventlog}
- <Footer/>
+ <Footer settings={this.state.settings}/>
</div>
);
}
diff --git a/web/src/js/components/virtualscroll.js b/web/src/js/components/virtualscroll.js
deleted file mode 100644
index f462fdcc..00000000
--- a/web/src/js/components/virtualscroll.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import React from "react";
-import ReactDOM from "react-dom";
-
-export var VirtualScrollMixin = {
- getInitialState: function () {
- return {
- start: 0,
- stop: 0
- };
- },
- componentWillMount: function () {
- if (!this.props.rowHeight) {
- console.warn("VirtualScrollMixin: No rowHeight specified", this);
- }
- },
- 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: Math.min(this.state.start, total) * this.props.rowHeight
- };
- var spacer = <Tag key="placeholder-top" style={style}></Tag>;
-
- if (this.state.start % 2 === 1) {
- // fix even/odd rows
- return [spacer, <Tag key="placeholder-top-2"></Tag>];
- } 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 <Tag key="placeholder-bottom" style={style}></Tag>;
- },
- componentDidMount: function () {
- this.onScroll();
- window.addEventListener('resize', this.onScroll);
- },
- componentWillUnmount: function(){
- window.removeEventListener('resize', this.onScroll);
- },
- onScroll: function () {
- var viewport = ReactDOM.findDOMNode(this);
- var top = viewport.scrollTop;
- var height = viewport.offsetHeight;
- var start = Math.floor(top / this.props.rowHeight);
- var stop = start + Math.ceil(height / (this.props.rowHeightMin || this.props.rowHeight));
-
- this.setState({
- start: start,
- stop: 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) {
-
- var row_top = (index * this.props.rowHeight) + head_height;
- var row_bottom = row_top + this.props.rowHeight;
-
- var viewport = ReactDOM.findDOMNode(this);
- 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;
- }
- },
-};