diff options
Diffstat (limited to 'web/src/js/components')
-rw-r--r-- | web/src/js/components/common.js | 135 | ||||
-rw-r--r-- | web/src/js/components/editor.js | 38 | ||||
-rw-r--r-- | web/src/js/components/eventlog.js | 244 | ||||
-rw-r--r-- | web/src/js/components/flowtable-columns.js | 13 | ||||
-rw-r--r-- | web/src/js/components/flowtable.js | 349 | ||||
-rw-r--r-- | web/src/js/components/flowview/contentview.js | 12 | ||||
-rw-r--r-- | web/src/js/components/flowview/details.js | 12 | ||||
-rw-r--r-- | web/src/js/components/flowview/index.js | 58 | ||||
-rw-r--r-- | web/src/js/components/flowview/messages.js | 68 | ||||
-rw-r--r-- | web/src/js/components/flowview/nav.js | 16 | ||||
-rw-r--r-- | web/src/js/components/footer.js | 35 | ||||
-rw-r--r-- | web/src/js/components/header.js | 89 | ||||
-rw-r--r-- | web/src/js/components/helpers/AutoScroll.js | 25 | ||||
-rw-r--r-- | web/src/js/components/helpers/VirtualScroll.js | 70 | ||||
-rw-r--r-- | web/src/js/components/mainview.js | 128 | ||||
-rw-r--r-- | web/src/js/components/prompt.js | 22 | ||||
-rw-r--r-- | web/src/js/components/proxyapp.js | 93 | ||||
-rw-r--r-- | web/src/js/components/virtualscroll.js | 85 |
18 files changed, 738 insertions, 754 deletions
diff --git a/web/src/js/components/common.js b/web/src/js/components/common.js index 965ae9a7..ad97ab38 100644 --- a/web/src/js/components/common.js +++ b/web/src/js/components/common.js @@ -1,109 +1,34 @@ -var React = require("react"); -var ReactRouter = require("react-router"); -var _ = require("lodash"); +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) -var AutoScrollMixin = { - componentWillUpdate: function () { - var node = this.getDOMNode(); - this._shouldScrollBottom = ( - node.scrollTop !== 0 && - node.scrollTop + node.clientHeight === node.scrollHeight - ); - }, - componentDidUpdate: function () { - if (this._shouldScrollBottom) { - var node = this.getDOMNode(); - node.scrollTop = node.scrollHeight; - } - }, -}; - - -var StickyHeadMixin = { - adjustHead: function () { - // Abusing CSS transforms to set the element - // referenced as head into some kind of position:sticky. - var head = this.refs.head.getDOMNode(); - head.style.transform = "translate(0," + this.getDOMNode().scrollTop + "px)"; - } -}; - -var SettingsState = { +export var Router = { 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 - }); + location: React.PropTypes.object, + router: React.PropTypes.object.isRequired }, -}; - - -var ChildFocus = { - contextTypes: { - returnFocus: React.PropTypes.func - }, - returnFocus: function(){ - React.findDOMNode(this).blur(); - window.getSelection().removeAllRanges(); - this.context.returnFocus(); - } -}; - - -var Navigation = _.extend({}, ReactRouter.Navigation, { - setQuery: function (dict) { - var q = this.context.router.getCurrentQuery(); - for (var i in dict) { - if (dict.hasOwnProperty(i)) { - q[i] = dict[i] || undefined; //falsey values shall be removed. + updateLocation: function (pathname, queryUpdate) { + if (pathname === undefined) { + pathname = this.context.location.pathname; + } + var query = this.context.location.query; + if (queryUpdate !== undefined) { + for (var i in queryUpdate) { + if (queryUpdate.hasOwnProperty(i)) { + query[i] = queryUpdate[i] || undefined; //falsey values shall be removed. + } } } - this.replaceWith(this.context.router.getCurrentPath(), this.context.router.getCurrentParams(), q); + this.context.router.replace({pathname, query}); }, - replaceWith: function (routeNameOrPath, params, query) { - if (routeNameOrPath === undefined) { - routeNameOrPath = this.context.router.getCurrentPath(); - } - if (params === undefined) { - params = this.context.router.getCurrentParams(); - } - if (query === undefined) { - query = this.context.router.getCurrentQuery(); - } - - this.context.router.replaceWith(routeNameOrPath, params, query); - } -}); - -// react-router is fairly good at changing its API regularly. -// We keep the old method for now - if it should turn out that their changes are permanent, -// we may remove this mixin and access react-router directly again. -var RouterState = _.extend({}, ReactRouter.State, { getQuery: function () { // 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.router.getCurrentQuery()); - }, - getParams: function () { - return _.clone(this.context.router.getCurrentParams()); + return _.clone(this.context.location.query); } -}); +}; -var Splitter = React.createClass({ +export var Splitter = React.createClass({ getDefaultProps: function () { return { axis: "x" @@ -127,7 +52,7 @@ var Splitter = React.createClass({ window.addEventListener("dragend", this.onDragEnd); }, onDragEnd: function () { - this.getDOMNode().style.transform = ""; + ReactDOM.findDOMNode(this).style.transform = ""; window.removeEventListener("dragend", this.onDragEnd); window.removeEventListener("mouseup", this.onMouseUp); window.removeEventListener("mousemove", this.onMouseMove); @@ -135,7 +60,7 @@ var Splitter = React.createClass({ onMouseUp: function (e) { this.onDragEnd(); - var node = this.getDOMNode(); + var node = ReactDOM.findDOMNode(this); var prev = node.previousElementSibling; var next = node.nextElementSibling; @@ -163,7 +88,7 @@ var Splitter = React.createClass({ } else { dY = e.pageY - this.state.startY; } - this.getDOMNode().style.transform = "translate(" + dX + "px," + dY + "px)"; + ReactDOM.findDOMNode(this).style.transform = "translate(" + dX + "px," + dY + "px)"; }, onResize: function () { // Trigger a global resize event. This notifies components that employ virtual scrolling @@ -176,7 +101,7 @@ var Splitter = React.createClass({ if (!this.state.applied) { return; } - var node = this.getDOMNode(); + var node = ReactDOM.findDOMNode(this); var prev = node.previousElementSibling; var next = node.nextElementSibling; @@ -206,14 +131,4 @@ var Splitter = React.createClass({ </div> ); } -}); - -module.exports = { - ChildFocus: ChildFocus, - RouterState: RouterState, - Navigation: Navigation, - StickyHeadMixin: StickyHeadMixin, - AutoScrollMixin: AutoScrollMixin, - Splitter: Splitter, - SettingsState: SettingsState -};
\ No newline at end of file +});
\ No newline at end of file diff --git a/web/src/js/components/editor.js b/web/src/js/components/editor.js index f2d44566..eed2f7c6 100644 --- a/web/src/js/components/editor.js +++ b/web/src/js/components/editor.js @@ -1,6 +1,6 @@ -var React = require("react"); -var common = require("./common.js"); -var utils = require("../utils.js"); +import React from "react"; +import ReactDOM from 'react-dom'; +import {Key} from "../utils.js"; var contentToHtml = function (content) { return _.escape(content); @@ -98,12 +98,12 @@ var EditorBase = React.createClass({ range = document.caretRangeFromPoint(e.clientX, e.clientY); } else { range = document.createRange(); - range.selectNodeContents(React.findDOMNode(this)); + range.selectNodeContents(ReactDOM.findDOMNode(this)); } this._ignore_events = true; this.setState({editable: true}, function () { - var node = React.findDOMNode(this); + var node = ReactDOM.findDOMNode(this); node.blur(); node.focus(); this._ignore_events = false; @@ -117,7 +117,7 @@ var EditorBase = React.createClass({ // a stop would cause a blur as a side-effect. // but a blur event must trigger a stop as well. // to fix this, make stop = blur and do the actual stop in the onBlur handler. - React.findDOMNode(this).blur(); + ReactDOM.findDOMNode(this).blur(); this.props.onStop && this.props.onStop(); }, _stop: function (e) { @@ -126,24 +126,24 @@ var EditorBase = React.createClass({ } console.log("_stop", _.extend({}, e)); window.getSelection().removeAllRanges(); //make sure that selection is cleared on blur - var node = React.findDOMNode(this); + var node = ReactDOM.findDOMNode(this); var content = this.props.nodeToContent(node); this.setState({editable: false}); this.props.onDone(content); this.props.onBlur && this.props.onBlur(e); }, reset: function () { - React.findDOMNode(this).innerHTML = this.props.contentToHtml(this.props.content); + ReactDOM.findDOMNode(this).innerHTML = this.props.contentToHtml(this.props.content); }, onKeyDown: function (e) { e.stopPropagation(); switch (e.keyCode) { - case utils.Key.ESC: + case Key.ESC: e.preventDefault(); this.reset(); this.stop(); break; - case utils.Key.ENTER: + case Key.ENTER: if (this.props.submitOnEnter && !e.shiftKey) { e.preventDefault(); this.stop(); @@ -154,7 +154,7 @@ var EditorBase = React.createClass({ } }, onInput: function () { - var node = React.findDOMNode(this); + var node = ReactDOM.findDOMNode(this); var content = this.props.nodeToContent(node); this.props.onInput && this.props.onInput(content); } @@ -212,8 +212,10 @@ var ValidateEditor = React.createClass({ /* Text Editor with mitmweb-specific convenience features */ -var ValueEditor = React.createClass({ - mixins: [common.ChildFocus], +export var ValueEditor = React.createClass({ + contextTypes: { + returnFocus: React.PropTypes.func + }, propTypes: { content: React.PropTypes.string.isRequired, onDone: React.PropTypes.func.isRequired, @@ -228,13 +230,9 @@ var ValueEditor = React.createClass({ />; }, focus: function () { - React.findDOMNode(this).focus(); + ReactDOM.findDOMNode(this).focus(); }, onStop: function () { - this.returnFocus(); + this.context.returnFocus(); } -}); - -module.exports = { - ValueEditor: ValueEditor -};
\ No newline at end of file +});
\ No newline at end of file diff --git a/web/src/js/components/eventlog.js b/web/src/js/components/eventlog.js index fea7b247..d1b23ace 100644 --- a/web/src/js/components/eventlog.js +++ b/web/src/js/components/eventlog.js @@ -1,115 +1,151 @@ -var React = require("react"); -var common = require("./common.js"); -var Query = require("../actions.js").Query; -var VirtualScrollMixin = require("./virtualscroll.js"); -var views = require("../store/view.js"); -var _ = require("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> +import React from "react" +import ReactDOM from "react-dom" +import shallowEqual from "shallowequal" +import {Router} from "./common.js" +import {Query} from "../actions.js" +import AutoScroll from "./helpers/AutoScroll"; +import {calcVScroll} from "./helpers/VirtualScroll" +import {StoreView} from "../store/view.js" +import _ from "lodash" + +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: [common.AutoScrollMixin, VirtualScrollMixin], - getInitialState: function () { - var filterFn = function (entry) { - return this.props.filter[entry.level]; - }; - var view = new views.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: [common.Navigation], - getInitialState: function () { + mixins: [Router], + getInitialState() { return { filter: { "debug": false, @@ -118,17 +154,17 @@ var EventLog = React.createClass({ } }; }, - close: function () { + close() { var d = {}; d[Query.SHOW_EVENTLOG] = undefined; - this.setQuery(d); + 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> @@ -141,10 +177,10 @@ var EventLog = React.createClass({ </div> </div> - <EventLogContents filter={this.state.filter}/> + <AutoScrollEventLog filter={this.state.filter}/> </div> ); } }); -module.exports = EventLog;
\ No newline at end of file +export default EventLog; diff --git a/web/src/js/components/flowtable-columns.js b/web/src/js/components/flowtable-columns.js index 74d96216..dbbe8847 100644 --- a/web/src/js/components/flowtable-columns.js +++ b/web/src/js/components/flowtable-columns.js @@ -1,7 +1,6 @@ -var React = require("react"); -var RequestUtils = require("../flow/utils.js").RequestUtils; -var ResponseUtils = require("../flow/utils.js").ResponseUtils; -var utils = require("../utils.js"); +import React from "react"; +import {RequestUtils, ResponseUtils} from "../flow/utils.js"; +import {formatSize, formatTimeDelta} from "../utils.js"; var TLSColumn = React.createClass({ statics: { @@ -156,7 +155,7 @@ var SizeColumn = React.createClass({ if (flow.response) { total += flow.response.contentLength || 0; } - var size = utils.formatSize(total); + var size = formatSize(total); return <td className="col-size">{size}</td>; } }); @@ -179,7 +178,7 @@ var TimeColumn = React.createClass({ var flow = this.props.flow; var time; if (flow.response) { - time = utils.formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start)); + time = formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start)); } else { time = "..."; } @@ -198,4 +197,4 @@ var all_columns = [ TimeColumn ]; -module.exports = all_columns; +export default all_columns; diff --git a/web/src/js/components/flowtable.js b/web/src/js/components/flowtable.js index 609034f6..f03b8ec0 100644 --- a/web/src/js/components/flowtable.js +++ b/web/src/js/components/flowtable.js @@ -1,187 +1,216 @@ -var React = require("react"); -var common = require("./common.js"); -var utils = require("../utils.js"); -var _ = require("lodash"); - -var VirtualScrollMixin = require("./virtualscroll.js"); -var flowtable_columns = require("./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"; - } +import React from "react"; +import ReactDOM from "react-dom"; +import classNames from "classnames"; +import {reverseString} from "../utils.js"; +import _ from "lodash"; +import shallowEqual from "shallowequal"; +import AutoScroll from "./helpers/AutoScroll"; +import {calcVScroll} from "./helpers/VirtualScroll"; +import flowtable_columns from "./flowtable-columns.js"; - 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) - //); +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, +}; + +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 utils.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: [common.StickyHeadMixin, common.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.index(flow), - this.refs.body.getDOMNode().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> ); } -}); +} -module.exports = FlowTable; +export default AutoScroll(FlowTable); diff --git a/web/src/js/components/flowview/contentview.js b/web/src/js/components/flowview/contentview.js index 63d22c1c..2743eec3 100644 --- a/web/src/js/components/flowview/contentview.js +++ b/web/src/js/components/flowview/contentview.js @@ -1,8 +1,8 @@ -var React = require("react"); -var _ = require("lodash"); +import React from "react"; +import _ from "lodash"; -var MessageUtils = require("../../flow/utils.js").MessageUtils; -var utils = require("../../utils.js"); +import {MessageUtils} from "../../flow/utils.js"; +import {formatSize} from "../../utils.js"; var image_regex = /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i; var ViewImage = React.createClass({ @@ -145,7 +145,7 @@ var TooLarge = React.createClass({ } }, render: function () { - var size = utils.formatSize(this.props.message.contentLength); + var size = formatSize(this.props.message.contentLength); return <div className="alert alert-warning"> <button onClick={this.props.onClick} className="btn btn-xs btn-warning pull-right">Display anyway</button> {size} content size. @@ -234,4 +234,4 @@ var ContentView = React.createClass({ } }); -module.exports = ContentView;
\ No newline at end of file +export default ContentView;
\ No newline at end of file diff --git a/web/src/js/components/flowview/details.js b/web/src/js/components/flowview/details.js index 00e0116c..45fe1292 100644 --- a/web/src/js/components/flowview/details.js +++ b/web/src/js/components/flowview/details.js @@ -1,7 +1,7 @@ -var React = require("react"); -var _ = require("lodash"); +import React from "react"; +import _ from "lodash"; -var utils = require("../../utils.js"); +import {formatTimeStamp, formatTimeDelta} from "../../utils.js"; var TimeStamp = React.createClass({ render: function () { @@ -11,11 +11,11 @@ var TimeStamp = React.createClass({ return <tr></tr>; } - var ts = utils.formatTimeStamp(this.props.t); + var ts = formatTimeStamp(this.props.t); var delta; if (this.props.deltaTo) { - delta = utils.formatTimeDelta(1000 * (this.props.t - this.props.deltaTo)); + delta = formatTimeDelta(1000 * (this.props.t - this.props.deltaTo)); delta = <span className="text-muted">{"(" + delta + ")"}</span>; } else { delta = null; @@ -178,4 +178,4 @@ var Details = React.createClass({ } }); -module.exports = Details;
\ No newline at end of file +export default Details;
\ No newline at end of file diff --git a/web/src/js/components/flowview/index.js b/web/src/js/components/flowview/index.js index 739a46dc..47531f58 100644 --- a/web/src/js/components/flowview/index.js +++ b/web/src/js/components/flowview/index.js @@ -1,22 +1,21 @@ -var React = require("react"); -var _ = require("lodash"); +import React from "react"; -var common = require("../common.js"); -var Nav = require("./nav.js"); -var Messages = require("./messages.js"); -var Details = require("./details.js"); -var Prompt = require("../prompt.js"); +import {Router, StickyHeadMixin} from "../common.js" +import Nav from "./nav.js"; +import {Request, Response, Error} from "./messages.js"; +import Details from "./details.js"; +import Prompt from "../prompt.js"; var allTabs = { - request: Messages.Request, - response: Messages.Response, - error: Messages.Error, + request: Request, + response: Response, + error: Error, details: Details }; var FlowView = React.createClass({ - mixins: [common.StickyHeadMixin, common.Navigation, common.RouterState], + mixins: [StickyHeadMixin, Router], getInitialState: function () { return { prompt: false @@ -34,37 +33,28 @@ var FlowView = React.createClass({ }, nextTab: function (i) { var tabs = this.getTabs(this.props.flow); - var currentIndex = tabs.indexOf(this.getActive()); + var currentIndex = tabs.indexOf(this.props.tab); // JS modulo operator doesn't correct negative numbers, make sure that we are positive. var nextIndex = (currentIndex + i + tabs.length) % tabs.length; this.selectTab(tabs[nextIndex]); }, selectTab: function (panel) { - this.replaceWith( - "flow", - { - flowId: this.getParams().flowId, - detailTab: panel - } - ); - }, - getActive: function(){ - return this.getParams().detailTab; + this.updateLocation(`/flows/${this.props.flow.id}/${panel}`); }, promptEdit: function () { var options; - switch(this.getActive()){ + switch (this.props.tab) { case "request": options = [ "method", "url", - {text:"http version", key:"v"}, + {text: "http version", key: "v"}, "header" /*, "content"*/]; break; case "response": options = [ - {text:"http version", key:"v"}, + {text: "http version", key: "v"}, "code", "message", "header" @@ -73,14 +63,14 @@ var FlowView = React.createClass({ case "details": return; default: - throw "Unknown tab for edit: " + this.getActive(); + throw "Unknown tab for edit: " + this.props.tab; } this.setState({ prompt: { done: function (k) { this.setState({prompt: false}); - if(k){ + if (k) { this.refs.tab.edit(k); } }.bind(this), @@ -91,9 +81,9 @@ var FlowView = React.createClass({ render: function () { var flow = this.props.flow; var tabs = this.getTabs(flow); - var active = this.getActive(); + var active = this.props.tab; - if (!_.contains(tabs, active)) { + if (tabs.indexOf(active) < 0) { if (active === "response" && flow.error) { active = "error"; } else if (active === "error" && flow.response) { @@ -113,10 +103,10 @@ var FlowView = React.createClass({ return ( <div className="flow-detail" onScroll={this.adjustHead}> <Nav ref="head" - flow={flow} - tabs={tabs} - active={active} - selectTab={this.selectTab}/> + flow={flow} + tabs={tabs} + active={active} + selectTab={this.selectTab}/> <Tab ref="tab" flow={flow}/> {prompt} </div> @@ -124,4 +114,4 @@ var FlowView = React.createClass({ } }); -module.exports = FlowView;
\ No newline at end of file +export default FlowView;
\ No newline at end of file diff --git a/web/src/js/components/flowview/messages.js b/web/src/js/components/flowview/messages.js index 7ac95d85..2885b3b1 100644 --- a/web/src/js/components/flowview/messages.js +++ b/web/src/js/components/flowview/messages.js @@ -1,12 +1,12 @@ -var React = require("react"); -var _ = require("lodash"); +import React from "react"; +import ReactDOM from 'react-dom'; +import _ from "lodash"; -var common = require("../common.js"); -var actions = require("../../actions.js"); -var flowutils = require("../../flow/utils.js"); -var utils = require("../../utils.js"); -var ContentView = require("./contentview.js"); -var ValueEditor = require("../editor.js").ValueEditor; +import {FlowActions} from "../../actions.js"; +import {RequestUtils, isValidHttpVersion, parseUrl, parseHttpVersion} from "../../flow/utils.js"; +import {Key, formatTimeStamp} from "../../utils.js"; +import ContentView from "./contentview.js"; +import {ValueEditor} from "../editor.js"; var Headers = React.createClass({ propTypes: { @@ -98,17 +98,17 @@ var HeaderEditor = React.createClass({ return <ValueEditor ref="input" {...this.props} onKeyDown={this.onKeyDown} inline/>; }, focus: function () { - this.getDOMNode().focus(); + ReactDOM.findDOMNode(this).focus(); }, onKeyDown: function (e) { switch (e.keyCode) { - case utils.Key.BACKSPACE: + case Key.BACKSPACE: var s = window.getSelection().getRangeAt(0); if (s.startOffset === 0 && s.endOffset === 0) { this.props.onRemove(e); } break; - case utils.Key.TAB: + case Key.TAB: if (!e.shiftKey) { this.props.onTab(e); } @@ -120,7 +120,7 @@ var HeaderEditor = React.createClass({ var RequestLine = React.createClass({ render: function () { var flow = this.props.flow; - var url = flowutils.RequestUtils.pretty_url(flow.request); + var url = RequestUtils.pretty_url(flow.request); var httpver = flow.request.http_version; return <div className="first-line request-line"> @@ -141,31 +141,31 @@ var RequestLine = React.createClass({ ref="httpVersion" content={httpver} onDone={this.onHttpVersionChange} - isValid={flowutils.isValidHttpVersion} + isValid={isValidHttpVersion} inline/> </div> }, isValidUrl: function (url) { - var u = flowutils.parseUrl(url); + var u = parseUrl(url); return !!u.host; }, onMethodChange: function (nextMethod) { - actions.FlowActions.update( + FlowActions.update( this.props.flow, {request: {method: nextMethod}} ); }, onUrlChange: function (nextUrl) { - var props = flowutils.parseUrl(nextUrl); + var props = parseUrl(nextUrl); props.path = props.path || ""; - actions.FlowActions.update( + FlowActions.update( this.props.flow, {request: props} ); }, onHttpVersionChange: function (nextVer) { - var ver = flowutils.parseHttpVersion(nextVer); - actions.FlowActions.update( + var ver = parseHttpVersion(nextVer); + FlowActions.update( this.props.flow, {request: {http_version: ver}} ); @@ -181,7 +181,7 @@ var ResponseLine = React.createClass({ ref="httpVersion" content={httpver} onDone={this.onHttpVersionChange} - isValid={flowutils.isValidHttpVersion} + isValid={isValidHttpVersion} inline/> <ValueEditor @@ -193,7 +193,7 @@ var ResponseLine = React.createClass({ <ValueEditor ref="msg" - content={flow.response.msg} + content={flow.response.reason} onDone={this.onMsgChange} inline/> </div>; @@ -202,28 +202,28 @@ var ResponseLine = React.createClass({ return /^\d+$/.test(code); }, onHttpVersionChange: function (nextVer) { - var ver = flowutils.parseHttpVersion(nextVer); - actions.FlowActions.update( + var ver = parseHttpVersion(nextVer); + FlowActions.update( this.props.flow, {response: {http_version: ver}} ); }, onMsgChange: function (nextMsg) { - actions.FlowActions.update( + FlowActions.update( this.props.flow, {response: {msg: nextMsg}} ); }, onCodeChange: function (nextCode) { nextCode = parseInt(nextCode); - actions.FlowActions.update( + FlowActions.update( this.props.flow, {response: {code: nextCode}} ); } }); -var Request = React.createClass({ +export var Request = React.createClass({ render: function () { var flow = this.props.flow; return ( @@ -255,7 +255,7 @@ var Request = React.createClass({ } }, onHeaderChange: function (nextHeaders) { - actions.FlowActions.update(this.props.flow, { + FlowActions.update(this.props.flow, { request: { headers: nextHeaders } @@ -263,7 +263,7 @@ var Request = React.createClass({ } }); -var Response = React.createClass({ +export var Response = React.createClass({ render: function () { var flow = this.props.flow; return ( @@ -295,7 +295,7 @@ var Response = React.createClass({ } }, onHeaderChange: function (nextHeaders) { - actions.FlowActions.update(this.props.flow, { + FlowActions.update(this.props.flow, { response: { headers: nextHeaders } @@ -303,7 +303,7 @@ var Response = React.createClass({ } }); -var Error = React.createClass({ +export var Error = React.createClass({ render: function () { var flow = this.props.flow; return ( @@ -311,16 +311,10 @@ var Error = React.createClass({ <div className="alert alert-warning"> {flow.error.msg} <div> - <small>{ utils.formatTimeStamp(flow.error.timestamp) }</small> + <small>{ formatTimeStamp(flow.error.timestamp) }</small> </div> </div> </section> ); } }); - -module.exports = { - Request: Request, - Response: Response, - Error: Error -}; diff --git a/web/src/js/components/flowview/nav.js b/web/src/js/components/flowview/nav.js index 46eda707..a12fd1fd 100644 --- a/web/src/js/components/flowview/nav.js +++ b/web/src/js/components/flowview/nav.js @@ -1,6 +1,6 @@ -var React = require("react"); +import React from "react"; -var actions = require("../../actions.js"); +import {FlowActions} from "../../actions.js"; var NavAction = React.createClass({ onClick: function (e) { @@ -38,19 +38,19 @@ var Nav = React.createClass({ var acceptButton = null; if(flow.intercepted){ - acceptButton = <NavAction title="[a]ccept intercepted flow" icon="fa-play" onClick={actions.FlowActions.accept.bind(null, flow)} />; + acceptButton = <NavAction title="[a]ccept intercepted flow" icon="fa-play" onClick={FlowActions.accept.bind(null, flow)} />; } var revertButton = null; if(flow.modified){ - revertButton = <NavAction title="revert changes to flow [V]" icon="fa-history" onClick={actions.FlowActions.revert.bind(null, flow)} />; + revertButton = <NavAction title="revert changes to flow [V]" icon="fa-history" onClick={FlowActions.revert.bind(null, flow)} />; } return ( <nav ref="head" className="nav-tabs nav-tabs-sm"> {tabs} - <NavAction title="[d]elete flow" icon="fa-trash" onClick={actions.FlowActions.delete.bind(null, flow)} /> - <NavAction title="[D]uplicate flow" icon="fa-copy" onClick={actions.FlowActions.duplicate.bind(null, flow)} /> - <NavAction disabled title="[r]eplay flow" icon="fa-repeat" onClick={actions.FlowActions.replay.bind(null, flow)} /> + <NavAction title="[d]elete flow" icon="fa-trash" onClick={FlowActions.delete.bind(null, flow)} /> + <NavAction title="[D]uplicate flow" icon="fa-copy" onClick={FlowActions.duplicate.bind(null, flow)} /> + <NavAction disabled title="[r]eplay flow" icon="fa-repeat" onClick={FlowActions.replay.bind(null, flow)} /> {acceptButton} {revertButton} </nav> @@ -58,4 +58,4 @@ var Nav = React.createClass({ } }); -module.exports = Nav;
\ No newline at end of file +export default Nav;
\ No newline at end of file diff --git a/web/src/js/components/footer.js b/web/src/js/components/footer.js index 229d691b..e2d96288 100644 --- a/web/src/js/components/footer.js +++ b/web/src/js/components/footer.js @@ -1,19 +1,20 @@ -var React = require("react"); -var common = require("./common.js"); +import React from "react"; +import {SettingsState} from "./common.js"; -var Footer = React.createClass({ - mixins: [common.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} - - {intercept ? <span className="label label-success">Intercept: {intercept}</span> : null} - </footer> - ); - } -}); +Footer.propTypes = { + settings: React.PropTypes.object.isRequired, +}; -module.exports = 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 998a41df..1af928a3 100644 --- a/web/src/js/components/header.js +++ b/web/src/js/components/header.js @@ -1,11 +1,12 @@ -var React = require("react"); -var $ = require("jquery"); +import React from "react"; +import ReactDOM from 'react-dom'; +import $ from "jquery"; -var Filt = require("../filt/filt.js"); -var utils = require("../utils.js"); -var common = require("./common.js"); -var actions = require("../actions.js"); -var Query = require("../actions.js").Query; +import Filt from "../filt/filt.js"; +import {Key} from "../utils.js"; +import {Router} from "./common.js"; +import {SettingsActions, FlowActions} from "../actions.js"; +import {Query} from "../actions.js"; var FilterDocs = React.createClass({ statics: { @@ -50,7 +51,9 @@ var FilterDocs = React.createClass({ } }); var FilterInput = React.createClass({ - mixins: [common.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 @@ -76,26 +79,24 @@ var FilterInput = React.createClass({ }, isValid: function (filt) { try { - Filt.parse(filt || this.state.value); + var str = filt || this.state.value; + if(str){ + Filt.parse(filt || this.state.value); + } return true; } catch (e) { return false; } }, getDesc: function () { - var desc; - try { - desc = Filt.parse(this.state.value).desc; - } catch (e) { - desc = "" + e; - } - if (desc !== "true") { - return desc; - } else { - return ( - <FilterDocs/> - ); + if(this.state.value) { + try { + return Filt.parse(this.state.value).desc; + } catch (e) { + return "" + e; + } } + return <FilterDocs/>; }, onFocus: function () { this.setState({focus: true}); @@ -110,7 +111,7 @@ var FilterInput = React.createClass({ this.setState({mousefocus: false}); }, onKeyDown: function (e) { - if (e.keyCode === utils.Key.ESC || e.keyCode === utils.Key.ENTER) { + if (e.keyCode === Key.ESC || e.keyCode === Key.ENTER) { this.blur(); // If closed using ESC/ENTER, hide the tooltip. this.setState({mousefocus: false}); @@ -118,11 +119,11 @@ var FilterInput = React.createClass({ e.stopPropagation(); }, blur: function () { - this.refs.input.getDOMNode().blur(); - this.returnFocus(); + ReactDOM.findDOMNode(this.refs.input).blur(); + this.context.returnFocus(); }, select: function () { - this.refs.input.getDOMNode().select(); + ReactDOM.findDOMNode(this.refs.input).select(); }, render: function () { var isValid = this.isValid(); @@ -159,8 +160,11 @@ var FilterInput = React.createClass({ } }); -var MainMenu = React.createClass({ - mixins: [common.Navigation, common.RouterState, common.SettingsState], +export var MainMenu = React.createClass({ + mixins: [Router], + propTypes: { + settings: React.PropTypes.object.isRequired, + }, statics: { title: "Start", route: "flows" @@ -168,20 +172,20 @@ var MainMenu = React.createClass({ onSearchChange: function (val) { var d = {}; d[Query.SEARCH] = val; - this.setQuery(d); + this.updateLocation(undefined, d); }, onHighlightChange: function (val) { var d = {}; d[Query.HIGHLIGHT] = val; - this.setQuery(d); + this.updateLocation(undefined, d); }, onInterceptChange: function (val) { - actions.SettingsActions.update({intercept: val}); + SettingsActions.update({intercept: val}); }, 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> @@ -220,7 +224,7 @@ var ViewMenu = React.createClass({ title: "View", route: "flows" }, - mixins: [common.Navigation, common.RouterState], + mixins: [Router], toggleEventLog: function () { var d = {}; @@ -230,7 +234,7 @@ var ViewMenu = React.createClass({ d[Query.SHOW_EVENTLOG] = "t"; // any non-false value will do it, keep it short } - this.setQuery(d); + this.updateLocation(undefined, d); }, render: function () { var showEventLog = this.getQuery()[Query.SHOW_EVENTLOG]; @@ -282,7 +286,7 @@ var FileMenu = React.createClass({ handleNewClick: function (e) { e.preventDefault(); if (confirm("Delete all flows?")) { - actions.FlowActions.clear(); + FlowActions.clear(); } }, handleOpenClick: function (e) { @@ -348,8 +352,11 @@ var FileMenu = React.createClass({ var header_entries = [MainMenu, ViewMenu /*, ReportsMenu */]; -var Header = React.createClass({ - mixins: [common.Navigation], +export var Header = React.createClass({ + mixins: [Router], + propTypes: { + settings: React.PropTypes.object.isRequired, + }, getInitialState: function () { return { active: header_entries[0] @@ -357,7 +364,7 @@ var Header = React.createClass({ }, handleClick: function (active, e) { e.preventDefault(); - this.replaceWith(active.route); + this.updateLocation(active.route); this.setState({active: active}); }, render: function () { @@ -385,15 +392,9 @@ 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> ); } }); - - -module.exports = { - Header: Header, - MainMenu: MainMenu -};
\ No newline at end of file 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..d9b31003 --- /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) + 2 + ); + + // 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 9ff51dfa..87c0c4bd 100644 --- a/web/src/js/components/mainview.js +++ b/web/src/js/components/mainview.js @@ -1,17 +1,16 @@ -var React = require("react"); +import React from "react"; -var actions = require("../actions.js"); -var Query = require("../actions.js").Query; -var utils = require("../utils.js"); -var views = require("../store/view.js"); -var Filt = require("../filt/filt.js"); - -var common = require("./common.js"); -var FlowTable = require("./flowtable.js"); -var FlowView = require("./flowview/index.js"); +import {FlowActions} from "../actions.js"; +import {Query} from "../actions.js"; +import {Key} from "../utils.js"; +import {StoreView} from "../store/view.js"; +import Filt from "../filt/filt.js"; +import { Router, Splitter} from "./common.js" +import FlowTable from "./flowtable.js"; +import FlowView from "./flowview/index.js"; var MainView = React.createClass({ - mixins: [common.Navigation, common.RouterState], + mixins: [Router], contextTypes: { flowStore: React.PropTypes.object.isRequired, }, @@ -25,7 +24,7 @@ var MainView = React.createClass({ }, getInitialState: function () { var sortKeyFun = false; - var view = new views.StoreView(this.context.flowStore, this.getViewFilt(), sortKeyFun); + var view = new StoreView(this.context.flowStore, this.getViewFilt(), sortKeyFun); view.addListener("recalculate", this.onRecalculate); view.addListener("add", this.onUpdate); view.addListener("update", this.onUpdate); @@ -42,24 +41,28 @@ var MainView = React.createClass({ }, getViewFilt: function () { try { - var filt = Filt.parse(this.getQuery()[Query.SEARCH] || ""); + var filtStr = this.getQuery()[Query.SEARCH]; + var filt = filtStr ? Filt.parse(filtStr) : () => true; var highlightStr = this.getQuery()[Query.HIGHLIGHT]; - var highlight = highlightStr ? Filt.parse(highlightStr) : false; + var highlight = highlightStr ? Filt.parse(highlightStr) : () => false; } catch (e) { console.error("Error when processing filter: " + e); } - return function filter_and_highlight(flow) { + var fun = function filter_and_highlight(flow) { if (!this._highlight) { this._highlight = {}; } - this._highlight[flow.id] = highlight && highlight(flow); + this._highlight[flow.id] = highlight(flow); return filt(flow); }; + fun.highlightStr = highlightStr; + fun.filtStr = filtStr; + return fun; }, componentWillReceiveProps: function (nextProps) { - var filterChanged = (this.props.query[Query.SEARCH] !== nextProps.query[Query.SEARCH]); - var highlightChanged = (this.props.query[Query.HIGHLIGHT] !== nextProps.query[Query.HIGHLIGHT]); + var filterChanged = this.state.view.filt.filtStr !== nextProps.location.query[Query.SEARCH]; + var highlightChanged = this.state.view.filt.highlightStr !== nextProps.location.query[Query.HIGHLIGHT]; if (filterChanged || highlightChanged) { this.state.view.recalculate(this.getViewFilt(), this.state.sortKeyFun); } @@ -72,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); } @@ -90,29 +93,24 @@ var MainView = React.createClass({ }, selectFlow: function (flow) { if (flow) { - this.replaceWith( - "flow", - { - flowId: flow.id, - detailTab: this.getParams().detailTab || "request" - } - ); + var tab = this.props.routeParams.detailTab || "request"; + this.updateLocation(`/flows/${flow.id}/${tab}`); this.refs.flowTable.scrollIntoView(flow); } else { - this.replaceWith("flows", {}); + this.updateLocation("/flows"); } }, 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) { @@ -132,80 +130,80 @@ var MainView = React.createClass({ return; } switch (e.keyCode) { - case utils.Key.K: - case utils.Key.UP: + case Key.K: + case Key.UP: this.selectFlowRelative(-1); break; - case utils.Key.J: - case utils.Key.DOWN: + case Key.J: + case Key.DOWN: this.selectFlowRelative(+1); break; - case utils.Key.SPACE: - case utils.Key.PAGE_DOWN: + case Key.SPACE: + case Key.PAGE_DOWN: this.selectFlowRelative(+10); break; - case utils.Key.PAGE_UP: + case Key.PAGE_UP: this.selectFlowRelative(-10); break; - case utils.Key.END: + case Key.END: this.selectFlowRelative(+1e10); break; - case utils.Key.HOME: + case Key.HOME: this.selectFlowRelative(-1e10); break; - case utils.Key.ESC: + case Key.ESC: this.selectFlow(null); break; - case utils.Key.H: - case utils.Key.LEFT: + case Key.H: + case Key.LEFT: if (this.refs.flowDetails) { this.refs.flowDetails.nextTab(-1); } break; - case utils.Key.L: - case utils.Key.TAB: - case utils.Key.RIGHT: + case Key.L: + case Key.TAB: + case Key.RIGHT: if (this.refs.flowDetails) { this.refs.flowDetails.nextTab(+1); } break; - case utils.Key.C: + case Key.C: if (e.shiftKey) { - actions.FlowActions.clear(); + FlowActions.clear(); } break; - case utils.Key.D: + case Key.D: if (flow) { if (e.shiftKey) { - actions.FlowActions.duplicate(flow); + FlowActions.duplicate(flow); } else { - actions.FlowActions.delete(flow); + FlowActions.delete(flow); } } break; - case utils.Key.A: + case Key.A: if (e.shiftKey) { - actions.FlowActions.accept_all(); + FlowActions.accept_all(); } else if (flow && flow.intercepted) { - actions.FlowActions.accept(flow); + FlowActions.accept(flow); } break; - case utils.Key.R: + case Key.R: if (!e.shiftKey && flow) { - actions.FlowActions.replay(flow); + FlowActions.replay(flow); } break; - case utils.Key.V: + case Key.V: if (e.shiftKey && flow && flow.modified) { - actions.FlowActions.revert(flow); + FlowActions.revert(flow); } break; - case utils.Key.E: + case Key.E: if (this.refs.flowDetails) { this.refs.flowDetails.promptEdit(); } break; - case utils.Key.SHIFT: + case Key.SHIFT: break; default: console.debug("keydown", e.keyCode); @@ -214,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(); @@ -222,8 +220,12 @@ var MainView = React.createClass({ var details; if (selected) { details = [ - <common.Splitter key="splitter"/>, - <FlowView key="flowDetails" ref="flowDetails" flow={selected}/> + <Splitter key="splitter"/>, + <FlowView + key="flowDetails" + ref="flowDetails" + tab={this.props.routeParams.detailTab} + flow={selected}/> ]; } else { details = null; @@ -241,4 +243,4 @@ var MainView = React.createClass({ } }); -module.exports = MainView; +export default MainView; diff --git a/web/src/js/components/prompt.js b/web/src/js/components/prompt.js index 121a1170..e324f7d4 100644 --- a/web/src/js/components/prompt.js +++ b/web/src/js/components/prompt.js @@ -1,18 +1,20 @@ -var React = require("react"); -var _ = require("lodash"); +import React from "react"; +import ReactDOM from 'react-dom'; +import _ from "lodash"; -var utils = require("../utils.js"); -var common = require("./common.js"); +import {Key} from "../utils.js"; var Prompt = React.createClass({ - mixins: [common.ChildFocus], + contextTypes: { + returnFocus: React.PropTypes.func + }, propTypes: { options: React.PropTypes.array.isRequired, done: React.PropTypes.func.isRequired, prompt: React.PropTypes.string }, componentDidMount: function () { - React.findDOMNode(this).focus(); + ReactDOM.findDOMNode(this).focus(); }, onKeyDown: function (e) { e.stopPropagation(); @@ -20,12 +22,12 @@ var Prompt = React.createClass({ var opts = this.getOptions(); for (var i = 0; i < opts.length; i++) { var k = opts[i].key; - if (utils.Key[k.toUpperCase()] === e.keyCode) { + if (Key[k.toUpperCase()] === e.keyCode) { this.done(k); return; } } - if (e.keyCode === utils.Key.ESC || e.keyCode === utils.Key.ENTER) { + if (e.keyCode === Key.ESC || e.keyCode === Key.ENTER) { this.done(false); } }, @@ -34,7 +36,7 @@ var Prompt = React.createClass({ }, done: function (ret) { this.props.done(ret); - this.returnFocus(); + this.context.returnFocus(); }, getOptions: function () { var opts = []; @@ -97,4 +99,4 @@ var Prompt = React.createClass({ } }); -module.exports = Prompt;
\ No newline at end of file +export default Prompt;
\ No newline at end of file diff --git a/web/src/js/components/proxyapp.js b/web/src/js/components/proxyapp.js index e766d6e6..d17a1522 100644 --- a/web/src/js/components/proxyapp.js +++ b/web/src/js/components/proxyapp.js @@ -1,15 +1,15 @@ -var React = require("react"); -var ReactRouter = require("react-router"); -var _ = require("lodash"); +import React from "react"; +import ReactDOM from "react-dom"; +import _ from "lodash"; -var common = require("./common.js"); -var MainView = require("./mainview.js"); -var Footer = require("./footer.js"); -var header = require("./header.js"); -var EventLog = require("./eventlog.js"); -var store = require("../store/store.js"); -var Query = require("../actions.js").Query; -var Key = require("../utils.js").Key; +import {Router, Splitter} from "./common.js" +import MainView from "./mainview.js"; +import Footer from "./footer.js"; +import {Header, MainMenu} from "./header.js"; +import EventLog from "./eventlog.js" +import {EventLogStore, FlowStore, SettingsStore} from "../store/store.js"; +import {Query} from "../actions.js"; +import {Key} from "../utils.js"; //TODO: Move out of here, just a stub. @@ -21,48 +21,58 @@ var Reports = React.createClass({ var ProxyAppMain = React.createClass({ - mixins: [common.RouterState], + mixins: [Router], childContextTypes: { - settingsStore: React.PropTypes.object.isRequired, flowStore: React.PropTypes.object.isRequired, eventStore: React.PropTypes.object.isRequired, returnFocus: React.PropTypes.func.isRequired, + location: React.PropTypes.object.isRequired, }, 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, + location: this.props.location }; }, getInitialState: function () { - var eventStore = new store.EventLogStore(); - var flowStore = new store.FlowStore(); - var settingsStore = new store.SettingsStore(); + var eventStore = new EventLogStore(); + 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 () { - React.findDOMNode(this).focus(); + document.activeElement.blur(); + window.getSelection().removeAllRanges(); + ReactDOM.findDOMNode(this).focus(); }, getMainComponent: function () { - return this.refs.view.refs.__routeHandler__; + return this.refs.view; }, onKeydown: function (e) { var selectFilterInput = function (name) { var headerComponent = this.refs.header; - headerComponent.setState({active: header.MainMenu}, function () { + headerComponent.setState({active: MainMenu}, function () { headerComponent.refs.active.refs[name].select(); }); }.bind(this); @@ -88,42 +98,39 @@ var ProxyAppMain = React.createClass({ }, render: function () { var eventlog; - if (this.getQuery()[Query.SHOW_EVENTLOG]) { + if (this.props.location.query[Query.SHOW_EVENTLOG]) { eventlog = [ - <common.Splitter key="splitter" axis="y"/>, + <Splitter key="splitter" axis="y"/>, <EventLog key="eventlog"/> ]; } else { eventlog = null; } + var children = React.cloneElement( + this.props.children, + { ref: "view", location: this.props.location } + ); return ( <div id="container" tabIndex="0" onKeyDown={this.onKeydown}> - <header.Header ref="header"/> - <RouteHandler ref="view" query={this.getQuery()}/> + <Header ref="header" settings={this.state.settings}/> + {children} {eventlog} - <Footer/> + <Footer settings={this.state.settings}/> </div> ); } }); -var Route = ReactRouter.Route; -var RouteHandler = ReactRouter.RouteHandler; -var Redirect = ReactRouter.Redirect; -var DefaultRoute = ReactRouter.DefaultRoute; -var NotFoundRoute = ReactRouter.NotFoundRoute; +import { Route, Router as ReactRouter, hashHistory, Redirect} from "react-router"; - -var routes = ( - <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" /> +export var app = ( +<ReactRouter history={hashHistory}> + <Redirect from="/" to="/flows" /> + <Route path="/" component={ProxyAppMain}> + <Route path="flows" component={MainView}/> + <Route path="flows/:flowId/:detailTab" component={MainView}/> + <Route path="reports" component={Reports}/> </Route> -); - -module.exports = { - routes: routes -};
\ No newline at end of file +</ReactRouter> +);
\ No newline at end of file diff --git a/web/src/js/components/virtualscroll.js b/web/src/js/components/virtualscroll.js deleted file mode 100644 index 956e1a0b..00000000 --- a/web/src/js/components/virtualscroll.js +++ /dev/null @@ -1,85 +0,0 @@ -var React = require("react"); - -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 = 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.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 = 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; - } - }, -}; - -module.exports = VirtualScrollMixin;
\ No newline at end of file |