diff --git a/libmproxy/web/static/app.css b/libmproxy/web/static/app.css index 2e71a32b9..a7dc4f001 100644 --- a/libmproxy/web/static/app.css +++ b/libmproxy/web/static/app.css @@ -351,6 +351,31 @@ header .menu { max-width: 100%; max-height: 100%; } +.prompt-dialog { + top: 0; + bottom: 0; + left: 0; + right: 0; + position: fixed; + z-index: 100; + background-color: rgba(0, 0, 0, 0.1); +} +.prompt-content { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 25px; + padding: 2px 5px; + background-color: white; + box-shadow: 0 -1px 3px lightgray; +} +.prompt-content .option { + cursor: pointer; +} +.prompt-content .option:not(:last-child)::after { + content: ", "; +} .eventlog { height: 200px; flex: 0 0 auto; diff --git a/libmproxy/web/static/app.js b/libmproxy/web/static/app.js index d5c495f3f..24082b406 100644 --- a/libmproxy/web/static/app.js +++ b/libmproxy/web/static/app.js @@ -440,17 +440,21 @@ module.exports = { Query: Query }; -},{"./dispatcher.js":19,"jquery":"jquery","lodash":"lodash"}],3:[function(require,module,exports){ - +},{"./dispatcher.js":20,"jquery":"jquery","lodash":"lodash"}],3:[function(require,module,exports){ var React = require("react"); var ReactRouter = require("react-router"); var $ = require("jquery"); var Connection = require("./connection"); var proxyapp = require("./components/proxyapp.js"); +var EventLogActions = require("./actions.js").EventLogActions; $(function () { window.ws = new Connection("/updates"); + window.onerror = function (msg) { + EventLogActions.add_event(msg); + }; + ReactRouter.run(proxyapp.routes, function (Handler, state) { React.render(React.createElement(Handler, null), document.body); }); @@ -458,7 +462,7 @@ $(function () { -},{"./components/proxyapp.js":16,"./connection":18,"jquery":"jquery","react":"react","react-router":"react-router"}],4:[function(require,module,exports){ +},{"./actions.js":2,"./components/proxyapp.js":17,"./connection":19,"jquery":"jquery","react":"react","react-router":"react-router"}],4:[function(require,module,exports){ var React = require("react"); var ReactRouter = require("react-router"); var _ = require("lodash"); @@ -721,7 +725,6 @@ var EventLogContents = React.createClass({displayName: "EventLogContents", view.addListener("recalculate", this.onEventLogChange); return { - log: view.list, view: view }; }, @@ -751,12 +754,13 @@ var EventLogContents = React.createClass({displayName: "EventLogContents", return React.createElement(LogMessage, {key: elem.id, entry: elem}); }, render: function () { - var rows = this.renderRows(this.state.log); + var entries = this.state.view.list; + var rows = this.renderRows(entries); return React.createElement("pre", {onScroll: this.onScroll}, - this.getPlaceholderTop(this.state.log.length), + this.getPlaceholderTop(entries.length), rows, - this.getPlaceholderBottom(this.state.log.length) + this.getPlaceholderBottom(entries.length) ); } }); @@ -825,7 +829,7 @@ var EventLog = React.createClass({displayName: "EventLog", module.exports = EventLog; -},{"../actions.js":2,"../store/view.js":23,"./common.js":4,"./virtualscroll.js":17,"lodash":"lodash","react":"react"}],6:[function(require,module,exports){ +},{"../actions.js":2,"../store/view.js":24,"./common.js":4,"./virtualscroll.js":18,"lodash":"lodash","react":"react"}],6:[function(require,module,exports){ var React = require("react"); var RequestUtils = require("../flow/utils.js").RequestUtils; var ResponseUtils = require("../flow/utils.js").ResponseUtils; @@ -1028,7 +1032,7 @@ var all_columns = [ module.exports = all_columns; -},{"../flow/utils.js":21,"../utils.js":24,"react":"react"}],7:[function(require,module,exports){ +},{"../flow/utils.js":22,"../utils.js":25,"react":"react"}],7:[function(require,module,exports){ var React = require("react"); var common = require("./common.js"); var utils = require("../utils.js"); @@ -1218,7 +1222,7 @@ var FlowTable = React.createClass({displayName: "FlowTable", module.exports = FlowTable; -},{"../utils.js":24,"./common.js":4,"./flowtable-columns.js":6,"./virtualscroll.js":17,"lodash":"lodash","react":"react"}],8:[function(require,module,exports){ +},{"../utils.js":25,"./common.js":4,"./flowtable-columns.js":6,"./virtualscroll.js":18,"lodash":"lodash","react":"react"}],8:[function(require,module,exports){ var React = require("react"); var _ = require("lodash"); @@ -1457,7 +1461,7 @@ var ContentView = React.createClass({displayName: "ContentView", module.exports = ContentView; -},{"../../flow/utils.js":21,"../../utils.js":24,"lodash":"lodash","react":"react"}],9:[function(require,module,exports){ +},{"../../flow/utils.js":22,"../../utils.js":25,"lodash":"lodash","react":"react"}],9:[function(require,module,exports){ var React = require("react"); var _ = require("lodash"); @@ -1640,7 +1644,7 @@ var Details = React.createClass({displayName: "Details", module.exports = Details; -},{"../../utils.js":24,"lodash":"lodash","react":"react"}],10:[function(require,module,exports){ +},{"../../utils.js":25,"lodash":"lodash","react":"react"}],10:[function(require,module,exports){ var React = require("react"); var _ = require("lodash"); @@ -1648,6 +1652,8 @@ 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"); + var allTabs = { request: Messages.Request, @@ -1658,6 +1664,11 @@ var allTabs = { var FlowView = React.createClass({displayName: "FlowView", mixins: [common.StickyHeadMixin, common.Navigation, common.RouterState], + getInitialState: function () { + return { + prompt: false + }; + }, getTabs: function (flow) { var tabs = []; ["request", "response", "error"].forEach(function (e) { @@ -1670,7 +1681,7 @@ var FlowView = React.createClass({displayName: "FlowView", }, nextTab: function (i) { var tabs = this.getTabs(this.props.flow); - var currentIndex = tabs.indexOf(this.getParams().detailTab); + var currentIndex = tabs.indexOf(this.getActive()); // 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]); @@ -1684,10 +1695,50 @@ var FlowView = React.createClass({displayName: "FlowView", } ); }, + getActive: function(){ + return this.getParams().detailTab; + }, + promptEdit: function () { + var options; + switch(this.getActive()){ + case "request": + options = [ + "method", + "url", + {text:"http version", key:"v"}, + "header" + /*, "content"*/]; + break; + case "response": + options = [ + {text:"http version", key:"v"}, + "code", + "message", + "header" + /*, "content"*/]; + break; + case "details": + return; + default: + throw "Unknown tab for edit: " + this.getActive(); + } + + this.setState({ + prompt: { + done: function (k) { + this.setState({prompt: false}); + if(k){ + this.refs.tab.edit(k); + } + }.bind(this), + options: options + } + }); + }, render: function () { var flow = this.props.flow; var tabs = this.getTabs(flow); - var active = this.getParams().detailTab; + var active = this.getActive(); if (!_.contains(tabs, active)) { if (active === "response" && flow.error) { @@ -1700,6 +1751,11 @@ var FlowView = React.createClass({displayName: "FlowView", this.selectTab(active); } + var prompt = null; + if (this.state.prompt) { + prompt = React.createElement(Prompt, React.__spread({}, this.state.prompt)); + } + var Tab = allTabs[active]; return ( React.createElement("div", {className: "flow-detail", onScroll: this.adjustHead}, @@ -1708,7 +1764,8 @@ var FlowView = React.createClass({displayName: "FlowView", tabs: tabs, active: active, selectTab: this.selectTab}), - React.createElement(Tab, {flow: flow}) + React.createElement(Tab, {ref: "tab", flow: flow}), + prompt ) ); } @@ -1716,7 +1773,7 @@ var FlowView = React.createClass({displayName: "FlowView", module.exports = FlowView; -},{"../common.js":4,"./details.js":9,"./messages.js":11,"./nav.js":12,"lodash":"lodash","react":"react"}],11:[function(require,module,exports){ +},{"../common.js":4,"../prompt.js":16,"./details.js":9,"./messages.js":11,"./nav.js":12,"lodash":"lodash","react":"react"}],11:[function(require,module,exports){ var React = require("react"); var _ = require("lodash"); @@ -1742,13 +1799,16 @@ var Headers = React.createClass({displayName: "Headers", } else { nextHeaders.splice(row, 1); // manually move selection target if this has been the last row. - if(row === nextHeaders.length){ - this._nextSel = (row-1)+"-value"; + if (row === nextHeaders.length) { + this._nextSel = (row - 1) + "-value"; } } } this.props.onChange(nextHeaders); }, + edit: function () { + this.refs["0-key"].focus(); + }, onTab: function (row, col, e) { var headers = this.props.message.headers; if (row === headers.length - 1 && col === 1) { @@ -1857,9 +1917,11 @@ var InlineInput = React.createClass({displayName: "InlineInput", }, blur: function () { this.getDOMNode().blur(); + window.getSelection().removeAllRanges(); this.context.returnFocus && this.context.returnFocus(); }, - selectContents: function () { + focus: function () { + React.findDOMNode(this).focus(); var range = document.createRange(); range.selectNodeContents(this.getDOMNode()); var sel = window.getSelection(); @@ -1867,7 +1929,7 @@ var InlineInput = React.createClass({displayName: "InlineInput", sel.addRange(range); }, onFocus: function () { - this.setState({editable: true}, this.selectContents); + this.setState({editable: true}, this.focus); }, onBlur: function (e) { this.setState({editable: false}); @@ -1901,7 +1963,7 @@ var HeaderInlineInput = React.createClass({displayName: "HeaderInlineInput", } break; case utils.Key.TAB: - if(!e.shiftKey){ + if (!e.shiftKey) { this.props.onTab(e); } break; @@ -1921,6 +1983,9 @@ var ValidateInlineInput = React.createClass({displayName: "ValidateInlineInput", originalContent: this.props.content }; }, + focus: function () { + this.getDOMNode().focus(); + }, onChange: function (val) { this.setState({ content: val @@ -1972,11 +2037,11 @@ var RequestLine = React.createClass({displayName: "RequestLine", var httpver = "HTTP/" + flow.request.httpversion.join("."); return React.createElement("div", {className: "first-line request-line"}, - React.createElement(InlineInput, {content: flow.request.method, onChange: this.onMethodChange}), + React.createElement(InlineInput, {ref: "method", content: flow.request.method, onChange: this.onMethodChange}), " ", - React.createElement(ValidateInlineInput, {content: url, onChange: this.onUrlChange, isValid: this.isValidUrl}), + React.createElement(ValidateInlineInput, {ref: "url", content: url, onChange: this.onUrlChange, isValid: this.isValidUrl}), " ", - React.createElement(ValidateInlineInput, {immediate: true, content: httpver, onChange: this.onHttpVersionChange, isValid: flowutils.isValidHttpVersion}) + React.createElement(ValidateInlineInput, {ref: "httpVersion", immediate: true, content: httpver, onChange: this.onHttpVersionChange, isValid: flowutils.isValidHttpVersion}) ) }, isValidUrl: function (url) { @@ -2011,11 +2076,11 @@ var ResponseLine = React.createClass({displayName: "ResponseLine", var flow = this.props.flow; var httpver = "HTTP/" + flow.response.httpversion.join("."); return React.createElement("div", {className: "first-line response-line"}, - React.createElement(ValidateInlineInput, {immediate: true, content: httpver, onChange: this.onHttpVersionChange, isValid: flowutils.isValidHttpVersion}), + React.createElement(ValidateInlineInput, {ref: "httpVersion", immediate: true, content: httpver, onChange: this.onHttpVersionChange, isValid: flowutils.isValidHttpVersion}), " ", - React.createElement(ValidateInlineInput, {immediate: true, content: flow.response.code + "", onChange: this.onCodeChange, isValid: this.isValidCode}), + React.createElement(ValidateInlineInput, {ref: "code", immediate: true, content: flow.response.code + "", onChange: this.onCodeChange, isValid: this.isValidCode}), " ", - React.createElement(InlineInput, {content: flow.response.msg, onChange: this.onMsgChange}) + React.createElement(InlineInput, {ref: "msg", content: flow.response.msg, onChange: this.onMsgChange}) ); }, @@ -2049,14 +2114,32 @@ var Request = React.createClass({displayName: "Request", var flow = this.props.flow; return ( React.createElement("section", {className: "request"}, - React.createElement(RequestLine, {flow: flow}), + React.createElement(RequestLine, {ref: "requestLine", flow: flow}), /**/ - React.createElement(Headers, {message: flow.request, onChange: this.onHeaderChange}), + React.createElement(Headers, {ref: "headers", message: flow.request, onChange: this.onHeaderChange}), React.createElement("hr", null), React.createElement(ContentView, {flow: flow, message: flow.request}) ) ); }, + edit: function (k) { + switch (k) { + case "m": + this.refs.requestLine.refs.method.focus(); + break; + case "u": + this.refs.requestLine.refs.url.focus(); + break; + case "v": + this.refs.requestLine.refs.httpVersion.focus(); + break; + case "h": + this.refs.headers.edit(); + break; + default: + throw "Unimplemented: "+ k; + } + }, onHeaderChange: function (nextHeaders) { actions.FlowActions.update(this.props.flow, { request: { @@ -2072,13 +2155,31 @@ var Response = React.createClass({displayName: "Response", return ( React.createElement("section", {className: "response"}, /**/ - React.createElement(ResponseLine, {flow: flow}), - React.createElement(Headers, {message: flow.response, onChange: this.onHeaderChange}), + React.createElement(ResponseLine, {ref: "responseLine", flow: flow}), + React.createElement(Headers, {ref: "headers", message: flow.response, onChange: this.onHeaderChange}), React.createElement("hr", null), React.createElement(ContentView, {flow: flow, message: flow.response}) ) ); }, + edit: function (k) { + switch (k) { + case "c": + this.refs.responseLine.refs.code.focus(); + break; + case "m": + this.refs.responseLine.refs.msg.focus(); + break; + case "v": + this.refs.responseLine.refs.httpVersion.focus(); + break; + case "h": + this.refs.headers.edit(); + break; + default: + throw "Unimplemented: "+ k; + } + }, onHeaderChange: function (nextHeaders) { actions.FlowActions.update(this.props.flow, { response: { @@ -2110,7 +2211,7 @@ module.exports = { Error: Error }; -},{"../../actions.js":2,"../../flow/utils.js":21,"../../utils.js":24,"../common.js":4,"./contentview.js":8,"lodash":"lodash","react":"react"}],12:[function(require,module,exports){ +},{"../../actions.js":2,"../../flow/utils.js":22,"../../utils.js":25,"../common.js":4,"./contentview.js":8,"lodash":"lodash","react":"react"}],12:[function(require,module,exports){ var React = require("react"); var actions = require("../../actions.js"); @@ -2184,7 +2285,7 @@ var Footer = React.createClass({displayName: "Footer", var intercept = this.state.settings.intercept; return ( React.createElement("footer", null, - mode != "regular" ? React.createElement("span", {className: "label label-success"}, mode, " mode") : null, + mode && mode != "regular" ? React.createElement("span", {className: "label label-success"}, mode, " mode") : null, " ", intercept ? React.createElement("span", {className: "label label-success"}, "Intercept: ", intercept) : null ) @@ -2362,7 +2463,7 @@ var MainMenu = React.createClass({displayName: "MainMenu", title: "Start", route: "flows" }, - onFilterChange: function (val) { + onSearchChange: function (val) { var d = {}; d[Query.SEARCH] = val; this.setQuery(d); @@ -2389,7 +2490,7 @@ var MainMenu = React.createClass({displayName: "MainMenu", type: "search", color: "black", value: search, - onChange: this.onFilterChange}), + onChange: this.onSearchChange}), React.createElement(FilterInput, { ref: "highlight", placeholder: "Highlight", @@ -2595,16 +2696,17 @@ module.exports = { MainMenu: MainMenu }; -},{"../actions.js":2,"../filt/filt.js":20,"../utils.js":24,"./common.js":4,"jquery":"jquery","react":"react"}],15:[function(require,module,exports){ +},{"../actions.js":2,"../filt/filt.js":21,"../utils.js":25,"./common.js":4,"jquery":"jquery","react":"react"}],15:[function(require,module,exports){ var React = require("react"); -var common = require("./common.js"); var actions = require("../actions.js"); var Query = require("../actions.js").Query; -var toputils = require("../utils.js"); +var utils = require("../utils.js"); var views = require("../store/view.js"); var Filt = require("../filt/filt.js"); -FlowTable = require("./flowtable.js"); + +var common = require("./common.js"); +var FlowTable = require("./flowtable.js"); var FlowView = require("./flowview/index.js"); var MainView = React.createClass({displayName: "MainView", @@ -2703,7 +2805,7 @@ var MainView = React.createClass({displayName: "MainView", var flows = this.state.view.list; var index; if (!this.getParams().flowId) { - if (shift > 0) { + if (shift < 0) { index = flows.length - 1; } else { index = 0; @@ -2729,49 +2831,49 @@ var MainView = React.createClass({displayName: "MainView", return; } switch (e.keyCode) { - case toputils.Key.K: - case toputils.Key.UP: + case utils.Key.K: + case utils.Key.UP: this.selectFlowRelative(-1); break; - case toputils.Key.J: - case toputils.Key.DOWN: + case utils.Key.J: + case utils.Key.DOWN: this.selectFlowRelative(+1); break; - case toputils.Key.SPACE: - case toputils.Key.PAGE_DOWN: + case utils.Key.SPACE: + case utils.Key.PAGE_DOWN: this.selectFlowRelative(+10); break; - case toputils.Key.PAGE_UP: + case utils.Key.PAGE_UP: this.selectFlowRelative(-10); break; - case toputils.Key.END: + case utils.Key.END: this.selectFlowRelative(+1e10); break; - case toputils.Key.HOME: + case utils.Key.HOME: this.selectFlowRelative(-1e10); break; - case toputils.Key.ESC: + case utils.Key.ESC: this.selectFlow(null); break; - case toputils.Key.H: - case toputils.Key.LEFT: + case utils.Key.H: + case utils.Key.LEFT: if (this.refs.flowDetails) { this.refs.flowDetails.nextTab(-1); } break; - case toputils.Key.L: - case toputils.Key.TAB: - case toputils.Key.RIGHT: + case utils.Key.L: + case utils.Key.TAB: + case utils.Key.RIGHT: if (this.refs.flowDetails) { this.refs.flowDetails.nextTab(+1); } break; - case toputils.Key.C: + case utils.Key.C: if (e.shiftKey) { actions.FlowActions.clear(); } break; - case toputils.Key.D: + case utils.Key.D: if (flow) { if (e.shiftKey) { actions.FlowActions.duplicate(flow); @@ -2780,24 +2882,29 @@ var MainView = React.createClass({displayName: "MainView", } } break; - case toputils.Key.A: + case utils.Key.A: if (e.shiftKey) { actions.FlowActions.accept_all(); } else if (flow && flow.intercepted) { actions.FlowActions.accept(flow); } break; - case toputils.Key.R: + case utils.Key.R: if (!e.shiftKey && flow) { actions.FlowActions.replay(flow); } break; - case toputils.Key.V: + case utils.Key.V: if (e.shiftKey && flow && flow.modified) { actions.FlowActions.revert(flow); } break; - case toputils.Key.SHIFT: + case utils.Key.E: + if (this.refs.flowDetails) { + this.refs.flowDetails.promptEdit(); + } + break; + case utils.Key.SHIFT: break; default: console.debug("keydown", e.keyCode); @@ -2836,7 +2943,109 @@ var MainView = React.createClass({displayName: "MainView", module.exports = MainView; -},{"../actions.js":2,"../filt/filt.js":20,"../store/view.js":23,"../utils.js":24,"./common.js":4,"./flowtable.js":7,"./flowview/index.js":10,"react":"react"}],16:[function(require,module,exports){ +},{"../actions.js":2,"../filt/filt.js":21,"../store/view.js":24,"../utils.js":25,"./common.js":4,"./flowtable.js":7,"./flowview/index.js":10,"react":"react"}],16:[function(require,module,exports){ +var React = require("react"); +var _ = require("lodash"); + +var utils = require("../utils.js"); +var common = require("./common.js"); + +var Prompt = React.createClass({displayName: "Prompt", + mixins: [common.ChildFocus], + propTypes: { + options: React.PropTypes.array.isRequired, + done: React.PropTypes.func.isRequired, + prompt: React.PropTypes.string + }, + componentDidMount: function () { + React.findDOMNode(this).focus(); + }, + onKeyDown: function (e) { + e.stopPropagation(); + e.preventDefault(); + var opts = this.getOptions(); + for (var i = 0; i < opts.length; i++) { + var k = opts[i].key; + if (utils.Key[k.toUpperCase()] === e.keyCode) { + this.done(k); + return; + } + } + if (e.keyCode === utils.Key.ESC || e.keyCode === utils.Key.ENTER) { + this.done(false); + } + }, + onClick: function (e) { + this.done(false); + }, + done: function (ret) { + this.props.done(ret); + this.context.returnFocus && this.context.returnFocus(); + }, + getOptions: function () { + var opts = []; + + var keyTaken = function (k) { + return _.includes(_.pluck(opts, "key"), k); + }; + + for (var i = 0; i < this.props.options.length; i++) { + var opt = this.props.options[i]; + if (_.isString(opt)) { + var str = opt; + while (str.length > 0 && keyTaken(str[0])) { + str = str.substr(1); + } + opt = { + text: opt, + key: str[0] + }; + } + if (!opt.text || !opt.key || keyTaken(opt.key)) { + throw "invalid options"; + } else { + opts.push(opt); + } + } + return opts; + }, + render: function () { + var opts = this.getOptions(); + opts = _.map(opts, function (o) { + var prefix, suffix; + var idx = o.text.indexOf(o.key); + if (idx !== -1) { + prefix = o.text.substring(0, idx); + suffix = o.text.substring(idx + 1); + + } else { + prefix = o.text + " ("; + suffix = ")"; + } + var onClick = function (e) { + this.done(o.key); + e.stopPropagation(); + }.bind(this); + return React.createElement("span", { + key: o.key, + className: "option", + onClick: onClick}, + prefix, + React.createElement("strong", {className: "text-primary"}, o.key), suffix + ); + }.bind(this)); + return React.createElement("div", {tabIndex: "0", onKeyDown: this.onKeyDown, onClick: this.onClick, className: "prompt-dialog"}, + React.createElement("div", {className: "prompt-content"}, + this.props.prompt || React.createElement("strong", null, "Select: "), + opts + ) + ); + } +}); + +module.exports = Prompt; + +},{"../utils.js":25,"./common.js":4,"lodash":"lodash","react":"react"}],17:[function(require,module,exports){ var React = require("react"); var ReactRouter = require("react-router"); var _ = require("lodash"); @@ -2911,7 +3120,7 @@ var ProxyAppMain = React.createClass({displayName: "ProxyAppMain", selectFilterInput("intercept"); break; case Key.L: - selectFilterInput("filter"); + selectFilterInput("search"); break; case Key.H: selectFilterInput("highlight"); @@ -2967,7 +3176,7 @@ module.exports = { routes: routes }; -},{"../actions.js":2,"../store/store.js":22,"../utils.js":24,"./common.js":4,"./eventlog.js":5,"./footer.js":13,"./header.js":14,"./mainview.js":15,"lodash":"lodash","react":"react","react-router":"react-router"}],17:[function(require,module,exports){ +},{"../actions.js":2,"../store/store.js":23,"../utils.js":25,"./common.js":4,"./eventlog.js":5,"./footer.js":13,"./header.js":14,"./mainview.js":15,"lodash":"lodash","react":"react","react-router":"react-router"}],18:[function(require,module,exports){ var React = require("react"); var VirtualScrollMixin = { @@ -3054,7 +3263,7 @@ var VirtualScrollMixin = { module.exports = VirtualScrollMixin; -},{"react":"react"}],18:[function(require,module,exports){ +},{"react":"react"}],19:[function(require,module,exports){ var actions = require("./actions.js"); var AppDispatcher = require("./dispatcher.js").AppDispatcher; @@ -3085,7 +3294,7 @@ function Connection(url) { module.exports = Connection; -},{"./actions.js":2,"./dispatcher.js":19}],19:[function(require,module,exports){ +},{"./actions.js":2,"./dispatcher.js":20}],20:[function(require,module,exports){ var flux = require("flux"); @@ -3109,7 +3318,7 @@ module.exports = { AppDispatcher: AppDispatcher }; -},{"flux":"flux"}],20:[function(require,module,exports){ +},{"flux":"flux"}],21:[function(require,module,exports){ module.exports = (function() { /* * Generated by PEG.js 0.8.0. @@ -4885,7 +5094,7 @@ module.exports = (function() { }; })(); -},{"../flow/utils.js":21}],21:[function(require,module,exports){ +},{"../flow/utils.js":22}],22:[function(require,module,exports){ var _ = require("lodash"); var $ = require("jquery"); @@ -5017,7 +5226,7 @@ module.exports = { isValidHttpVersion: isValidHttpVersion }; -},{"jquery":"jquery","lodash":"lodash"}],22:[function(require,module,exports){ +},{"jquery":"jquery","lodash":"lodash"}],23:[function(require,module,exports){ var _ = require("lodash"); var $ = require("jquery"); @@ -5200,7 +5409,7 @@ module.exports = { FlowStore: FlowStore }; -},{"../actions.js":2,"../dispatcher.js":19,"../utils.js":24,"events":1,"jquery":"jquery","lodash":"lodash"}],23:[function(require,module,exports){ +},{"../actions.js":2,"../dispatcher.js":20,"../utils.js":25,"events":1,"jquery":"jquery","lodash":"lodash"}],24:[function(require,module,exports){ var EventEmitter = require('events').EventEmitter; var _ = require("lodash"); @@ -5317,15 +5526,14 @@ module.exports = { StoreView: StoreView }; -},{"../utils.js":24,"events":1,"lodash":"lodash"}],24:[function(require,module,exports){ +},{"../utils.js":25,"events":1,"lodash":"lodash"}],25:[function(require,module,exports){ var $ = require("jquery"); var _ = require("lodash"); var actions = require("./actions.js"); -//Debug (don't expose by default, this increases compile time drastically) -//window.$ = $; -//window._ = _; -//window.React = require("React"); +window.$ = $; +window._ = _; +window.React = require("react"); var Key = { UP: 38, @@ -5432,7 +5640,7 @@ module.exports = { Key: Key, }; -},{"./actions.js":2,"jquery":"jquery","lodash":"lodash"}]},{},[3]) +},{"./actions.js":2,"jquery":"jquery","lodash":"lodash","react":"react"}]},{},[3]) //# sourceMappingURL=app.js.map \ No newline at end of file diff --git a/web/src/css/app.less b/web/src/css/app.less index ecec3d9c2..046d378a1 100644 --- a/web/src/css/app.less +++ b/web/src/css/app.less @@ -14,5 +14,6 @@ html { @import (less) "flowtable.less"; @import (less) "flowdetail.less"; @import (less) "flowview.less"; +@import (less) "prompt.less"; @import (less) "eventlog.less"; @import (less) "footer.less"; \ No newline at end of file diff --git a/web/src/css/prompt.less b/web/src/css/prompt.less new file mode 100644 index 000000000..eee0426dd --- /dev/null +++ b/web/src/css/prompt.less @@ -0,0 +1,27 @@ +.prompt-dialog { + top: 0; + bottom: 0; + left: 0; + right: 0; + position: fixed; + z-index: 100; + background-color: rgba(0, 0, 0, 0.1); +} + +.prompt-content { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 25px; + padding: 2px 5px; + background-color: white; + box-shadow: 0 -1px 3px lightgray; + + .option { + cursor: pointer; + &:not(:last-child)::after { + content: ", "; + } + } +} \ No newline at end of file diff --git a/web/src/js/app.js b/web/src/js/app.js index 63d782d44..9fb868b80 100644 --- a/web/src/js/app.js +++ b/web/src/js/app.js @@ -1,13 +1,17 @@ - var React = require("react"); var ReactRouter = require("react-router"); var $ = require("jquery"); var Connection = require("./connection"); var proxyapp = require("./components/proxyapp.js"); +var EventLogActions = require("./actions.js").EventLogActions; $(function () { window.ws = new Connection("/updates"); + window.onerror = function (msg) { + EventLogActions.add_event(msg); + }; + ReactRouter.run(proxyapp.routes, function (Handler, state) { React.render(, document.body); }); diff --git a/web/src/js/components/eventlog.js b/web/src/js/components/eventlog.js index 65024712b..4c6dafb1c 100644 --- a/web/src/js/components/eventlog.js +++ b/web/src/js/components/eventlog.js @@ -44,7 +44,6 @@ var EventLogContents = React.createClass({ view.addListener("recalculate", this.onEventLogChange); return { - log: view.list, view: view }; }, @@ -74,12 +73,13 @@ var EventLogContents = React.createClass({ return ; }, render: function () { - var rows = this.renderRows(this.state.log); + var entries = this.state.view.list; + var rows = this.renderRows(entries); return
-            { this.getPlaceholderTop(this.state.log.length) }
+            { this.getPlaceholderTop(entries.length) }
             {rows}
-            { this.getPlaceholderBottom(this.state.log.length) }
+            { this.getPlaceholderBottom(entries.length) }
         
; } }); diff --git a/web/src/js/components/flowview/index.js b/web/src/js/components/flowview/index.js index 4214714ef..739a46dc6 100644 --- a/web/src/js/components/flowview/index.js +++ b/web/src/js/components/flowview/index.js @@ -5,6 +5,8 @@ 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"); + var allTabs = { request: Messages.Request, @@ -15,6 +17,11 @@ var allTabs = { var FlowView = React.createClass({ mixins: [common.StickyHeadMixin, common.Navigation, common.RouterState], + getInitialState: function () { + return { + prompt: false + }; + }, getTabs: function (flow) { var tabs = []; ["request", "response", "error"].forEach(function (e) { @@ -27,7 +34,7 @@ var FlowView = React.createClass({ }, nextTab: function (i) { var tabs = this.getTabs(this.props.flow); - var currentIndex = tabs.indexOf(this.getParams().detailTab); + var currentIndex = tabs.indexOf(this.getActive()); // 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]); @@ -41,10 +48,50 @@ var FlowView = React.createClass({ } ); }, + getActive: function(){ + return this.getParams().detailTab; + }, + promptEdit: function () { + var options; + switch(this.getActive()){ + case "request": + options = [ + "method", + "url", + {text:"http version", key:"v"}, + "header" + /*, "content"*/]; + break; + case "response": + options = [ + {text:"http version", key:"v"}, + "code", + "message", + "header" + /*, "content"*/]; + break; + case "details": + return; + default: + throw "Unknown tab for edit: " + this.getActive(); + } + + this.setState({ + prompt: { + done: function (k) { + this.setState({prompt: false}); + if(k){ + this.refs.tab.edit(k); + } + }.bind(this), + options: options + } + }); + }, render: function () { var flow = this.props.flow; var tabs = this.getTabs(flow); - var active = this.getParams().detailTab; + var active = this.getActive(); if (!_.contains(tabs, active)) { if (active === "response" && flow.error) { @@ -57,6 +104,11 @@ var FlowView = React.createClass({ this.selectTab(active); } + var prompt = null; + if (this.state.prompt) { + prompt = ; + } + var Tab = allTabs[active]; return (
@@ -65,7 +117,8 @@ var FlowView = React.createClass({ tabs={tabs} active={active} selectTab={this.selectTab}/> - + + {prompt}
); } diff --git a/web/src/js/components/flowview/messages.js b/web/src/js/components/flowview/messages.js index 7feaa6340..cb166026a 100644 --- a/web/src/js/components/flowview/messages.js +++ b/web/src/js/components/flowview/messages.js @@ -23,13 +23,16 @@ var Headers = React.createClass({ } else { nextHeaders.splice(row, 1); // manually move selection target if this has been the last row. - if(row === nextHeaders.length){ - this._nextSel = (row-1)+"-value"; + if (row === nextHeaders.length) { + this._nextSel = (row - 1) + "-value"; } } } this.props.onChange(nextHeaders); }, + edit: function () { + this.refs["0-key"].focus(); + }, onTab: function (row, col, e) { var headers = this.props.message.headers; if (row === headers.length - 1 && col === 1) { @@ -138,9 +141,11 @@ var InlineInput = React.createClass({ }, blur: function () { this.getDOMNode().blur(); + window.getSelection().removeAllRanges(); this.context.returnFocus && this.context.returnFocus(); }, - selectContents: function () { + focus: function () { + React.findDOMNode(this).focus(); var range = document.createRange(); range.selectNodeContents(this.getDOMNode()); var sel = window.getSelection(); @@ -148,7 +153,7 @@ var InlineInput = React.createClass({ sel.addRange(range); }, onFocus: function () { - this.setState({editable: true}, this.selectContents); + this.setState({editable: true}, this.focus); }, onBlur: function (e) { this.setState({editable: false}); @@ -182,7 +187,7 @@ var HeaderInlineInput = React.createClass({ } break; case utils.Key.TAB: - if(!e.shiftKey){ + if (!e.shiftKey) { this.props.onTab(e); } break; @@ -202,6 +207,9 @@ var ValidateInlineInput = React.createClass({ originalContent: this.props.content }; }, + focus: function () { + this.getDOMNode().focus(); + }, onChange: function (val) { this.setState({ content: val @@ -253,11 +261,11 @@ var RequestLine = React.createClass({ var httpver = "HTTP/" + flow.request.httpversion.join("."); return
- +   - +   - +
}, isValidUrl: function (url) { @@ -292,11 +300,11 @@ var ResponseLine = React.createClass({ var flow = this.props.flow; var httpver = "HTTP/" + flow.response.httpversion.join("."); return
- +   - +   - +
; }, @@ -330,14 +338,32 @@ var Request = React.createClass({ var flow = this.props.flow; return (
- + {/**/} - +
); }, + edit: function (k) { + switch (k) { + case "m": + this.refs.requestLine.refs.method.focus(); + break; + case "u": + this.refs.requestLine.refs.url.focus(); + break; + case "v": + this.refs.requestLine.refs.httpVersion.focus(); + break; + case "h": + this.refs.headers.edit(); + break; + default: + throw "Unimplemented: "+ k; + } + }, onHeaderChange: function (nextHeaders) { actions.FlowActions.update(this.props.flow, { request: { @@ -353,13 +379,31 @@ var Response = React.createClass({ return (
{/**/} - - + +
); }, + edit: function (k) { + switch (k) { + case "c": + this.refs.responseLine.refs.code.focus(); + break; + case "m": + this.refs.responseLine.refs.msg.focus(); + break; + case "v": + this.refs.responseLine.refs.httpVersion.focus(); + break; + case "h": + this.refs.headers.edit(); + break; + default: + throw "Unimplemented: "+ k; + } + }, onHeaderChange: function (nextHeaders) { actions.FlowActions.update(this.props.flow, { response: { diff --git a/web/src/js/components/footer.js b/web/src/js/components/footer.js index 52d6d0ad7..229d691b9 100644 --- a/web/src/js/components/footer.js +++ b/web/src/js/components/footer.js @@ -8,7 +8,7 @@ var Footer = React.createClass({ var intercept = this.state.settings.intercept; return (
- {mode != "regular" ? {mode} mode : null} + {mode && mode != "regular" ? {mode} mode : null}   {intercept ? Intercept: {intercept} : null}
diff --git a/web/src/js/components/header.js b/web/src/js/components/header.js index 6cd1006dc..225f5b9ff 100644 --- a/web/src/js/components/header.js +++ b/web/src/js/components/header.js @@ -165,7 +165,7 @@ var MainMenu = React.createClass({ title: "Start", route: "flows" }, - onFilterChange: function (val) { + onSearchChange: function (val) { var d = {}; d[Query.SEARCH] = val; this.setQuery(d); @@ -192,7 +192,7 @@ var MainMenu = React.createClass({ type="search" color="black" value={search} - onChange={this.onFilterChange} /> + onChange={this.onSearchChange} /> 0) { + if (shift < 0) { index = flows.length - 1; } else { index = 0; @@ -131,49 +132,49 @@ var MainView = React.createClass({ return; } switch (e.keyCode) { - case toputils.Key.K: - case toputils.Key.UP: + case utils.Key.K: + case utils.Key.UP: this.selectFlowRelative(-1); break; - case toputils.Key.J: - case toputils.Key.DOWN: + case utils.Key.J: + case utils.Key.DOWN: this.selectFlowRelative(+1); break; - case toputils.Key.SPACE: - case toputils.Key.PAGE_DOWN: + case utils.Key.SPACE: + case utils.Key.PAGE_DOWN: this.selectFlowRelative(+10); break; - case toputils.Key.PAGE_UP: + case utils.Key.PAGE_UP: this.selectFlowRelative(-10); break; - case toputils.Key.END: + case utils.Key.END: this.selectFlowRelative(+1e10); break; - case toputils.Key.HOME: + case utils.Key.HOME: this.selectFlowRelative(-1e10); break; - case toputils.Key.ESC: + case utils.Key.ESC: this.selectFlow(null); break; - case toputils.Key.H: - case toputils.Key.LEFT: + case utils.Key.H: + case utils.Key.LEFT: if (this.refs.flowDetails) { this.refs.flowDetails.nextTab(-1); } break; - case toputils.Key.L: - case toputils.Key.TAB: - case toputils.Key.RIGHT: + case utils.Key.L: + case utils.Key.TAB: + case utils.Key.RIGHT: if (this.refs.flowDetails) { this.refs.flowDetails.nextTab(+1); } break; - case toputils.Key.C: + case utils.Key.C: if (e.shiftKey) { actions.FlowActions.clear(); } break; - case toputils.Key.D: + case utils.Key.D: if (flow) { if (e.shiftKey) { actions.FlowActions.duplicate(flow); @@ -182,24 +183,29 @@ var MainView = React.createClass({ } } break; - case toputils.Key.A: + case utils.Key.A: if (e.shiftKey) { actions.FlowActions.accept_all(); } else if (flow && flow.intercepted) { actions.FlowActions.accept(flow); } break; - case toputils.Key.R: + case utils.Key.R: if (!e.shiftKey && flow) { actions.FlowActions.replay(flow); } break; - case toputils.Key.V: + case utils.Key.V: if (e.shiftKey && flow && flow.modified) { actions.FlowActions.revert(flow); } break; - case toputils.Key.SHIFT: + case utils.Key.E: + if (this.refs.flowDetails) { + this.refs.flowDetails.promptEdit(); + } + break; + case utils.Key.SHIFT: break; default: console.debug("keydown", e.keyCode); diff --git a/web/src/js/components/prompt.js b/web/src/js/components/prompt.js new file mode 100644 index 000000000..229e82c85 --- /dev/null +++ b/web/src/js/components/prompt.js @@ -0,0 +1,100 @@ +var React = require("react"); +var _ = require("lodash"); + +var utils = require("../utils.js"); +var common = require("./common.js"); + +var Prompt = React.createClass({ + mixins: [common.ChildFocus], + propTypes: { + options: React.PropTypes.array.isRequired, + done: React.PropTypes.func.isRequired, + prompt: React.PropTypes.string + }, + componentDidMount: function () { + React.findDOMNode(this).focus(); + }, + onKeyDown: function (e) { + e.stopPropagation(); + e.preventDefault(); + var opts = this.getOptions(); + for (var i = 0; i < opts.length; i++) { + var k = opts[i].key; + if (utils.Key[k.toUpperCase()] === e.keyCode) { + this.done(k); + return; + } + } + if (e.keyCode === utils.Key.ESC || e.keyCode === utils.Key.ENTER) { + this.done(false); + } + }, + onClick: function (e) { + this.done(false); + }, + done: function (ret) { + this.props.done(ret); + this.context.returnFocus && this.context.returnFocus(); + }, + getOptions: function () { + var opts = []; + + var keyTaken = function (k) { + return _.includes(_.pluck(opts, "key"), k); + }; + + for (var i = 0; i < this.props.options.length; i++) { + var opt = this.props.options[i]; + if (_.isString(opt)) { + var str = opt; + while (str.length > 0 && keyTaken(str[0])) { + str = str.substr(1); + } + opt = { + text: opt, + key: str[0] + }; + } + if (!opt.text || !opt.key || keyTaken(opt.key)) { + throw "invalid options"; + } else { + opts.push(opt); + } + } + return opts; + }, + render: function () { + var opts = this.getOptions(); + opts = _.map(opts, function (o) { + var prefix, suffix; + var idx = o.text.indexOf(o.key); + if (idx !== -1) { + prefix = o.text.substring(0, idx); + suffix = o.text.substring(idx + 1); + + } else { + prefix = o.text + " ("; + suffix = ")"; + } + var onClick = function (e) { + this.done(o.key); + e.stopPropagation(); + }.bind(this); + return + {prefix} + {o.key}{suffix} + ; + }.bind(this)); + return
+
+ {this.props.prompt || Select: } + {opts} +
+
; + } +}); + +module.exports = Prompt; \ No newline at end of file diff --git a/web/src/js/components/proxyapp.js b/web/src/js/components/proxyapp.js index ead6f7e84..e766d6e60 100644 --- a/web/src/js/components/proxyapp.js +++ b/web/src/js/components/proxyapp.js @@ -72,7 +72,7 @@ var ProxyAppMain = React.createClass({ selectFilterInput("intercept"); break; case Key.L: - selectFilterInput("filter"); + selectFilterInput("search"); break; case Key.H: selectFilterInput("highlight"); diff --git a/web/src/js/utils.js b/web/src/js/utils.js index d848ff243..405756928 100644 --- a/web/src/js/utils.js +++ b/web/src/js/utils.js @@ -2,10 +2,9 @@ var $ = require("jquery"); var _ = require("lodash"); var actions = require("./actions.js"); -//Debug (don't expose by default, this increases compile time drastically) -//window.$ = $; -//window._ = _; -//window.React = require("React"); +window.$ = $; +window._ = _; +window.React = require("react"); var Key = { UP: 38,