aboutsummaryrefslogtreecommitdiffstats
path: root/web/src
diff options
context:
space:
mode:
authorMaximilian Hils <git@maximilianhils.com>2015-03-30 03:49:50 +0200
committerMaximilian Hils <git@maximilianhils.com>2015-03-30 03:49:50 +0200
commit6d29f93e9eaaabe20c0d46887048f2367bfbb3cf (patch)
tree0b3be8ff99d7b986596ca69e67b3e7fe885d4764 /web/src
parent737002921e9aed701afb49fda8777e8c9286bf09 (diff)
downloadmitmproxy-6d29f93e9eaaabe20c0d46887048f2367bfbb3cf.tar.gz
mitmproxy-6d29f93e9eaaabe20c0d46887048f2367bfbb3cf.tar.bz2
mitmproxy-6d29f93e9eaaabe20c0d46887048f2367bfbb3cf.zip
web: add prompt for keyboard navigation
Diffstat (limited to 'web/src')
-rw-r--r--web/src/css/app.less1
-rw-r--r--web/src/css/prompt.less27
-rw-r--r--web/src/js/app.js6
-rw-r--r--web/src/js/components/eventlog.js8
-rw-r--r--web/src/js/components/flowview/index.js59
-rw-r--r--web/src/js/components/flowview/messages.js74
-rw-r--r--web/src/js/components/footer.js2
-rw-r--r--web/src/js/components/header.js4
-rw-r--r--web/src/js/components/mainview.js56
-rw-r--r--web/src/js/components/prompt.js100
-rw-r--r--web/src/js/components/proxyapp.js2
-rw-r--r--web/src/js/utils.js7
12 files changed, 290 insertions, 56 deletions
diff --git a/web/src/css/app.less b/web/src/css/app.less
index ecec3d9c..046d378a 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 00000000..eee0426d
--- /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 63d782d4..9fb868b8 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(<Handler/>, document.body);
});
diff --git a/web/src/js/components/eventlog.js b/web/src/js/components/eventlog.js
index 65024712..4c6dafb1 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 <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 <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) }
</pre>;
}
});
diff --git a/web/src/js/components/flowview/index.js b/web/src/js/components/flowview/index.js
index 4214714e..739a46dc 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 = <Prompt {...this.state.prompt}/>;
+ }
+
var Tab = allTabs[active];
return (
<div className="flow-detail" onScroll={this.adjustHead}>
@@ -65,7 +117,8 @@ var FlowView = React.createClass({
tabs={tabs}
active={active}
selectTab={this.selectTab}/>
- <Tab flow={flow}/>
+ <Tab ref="tab" flow={flow}/>
+ {prompt}
</div>
);
}
diff --git a/web/src/js/components/flowview/messages.js b/web/src/js/components/flowview/messages.js
index 7feaa634..cb166026 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 <div className="first-line request-line">
- <InlineInput content={flow.request.method} onChange={this.onMethodChange}/>
+ <InlineInput ref="method" content={flow.request.method} onChange={this.onMethodChange}/>
&nbsp;
- <ValidateInlineInput content={url} onChange={this.onUrlChange} isValid={this.isValidUrl} />
+ <ValidateInlineInput ref="url" content={url} onChange={this.onUrlChange} isValid={this.isValidUrl} />
&nbsp;
- <ValidateInlineInput immediate content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} />
+ <ValidateInlineInput ref="httpVersion" immediate content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} />
</div>
},
isValidUrl: function (url) {
@@ -292,11 +300,11 @@ var ResponseLine = React.createClass({
var flow = this.props.flow;
var httpver = "HTTP/" + flow.response.httpversion.join(".");
return <div className="first-line response-line">
- <ValidateInlineInput immediate content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} />
+ <ValidateInlineInput ref="httpVersion" immediate content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} />
&nbsp;
- <ValidateInlineInput immediate content={flow.response.code + ""} onChange={this.onCodeChange} isValid={this.isValidCode} />
+ <ValidateInlineInput ref="code" immediate content={flow.response.code + ""} onChange={this.onCodeChange} isValid={this.isValidCode} />
&nbsp;
- <InlineInput content={flow.response.msg} onChange={this.onMsgChange}/>
+ <InlineInput ref="msg" content={flow.response.msg} onChange={this.onMsgChange}/>
</div>;
},
@@ -330,14 +338,32 @@ var Request = React.createClass({
var flow = this.props.flow;
return (
<section className="request">
- <RequestLine flow={flow}/>
+ <RequestLine ref="requestLine" flow={flow}/>
{/*<ResponseLine flow={flow}/>*/}
- <Headers message={flow.request} onChange={this.onHeaderChange}/>
+ <Headers ref="headers" message={flow.request} onChange={this.onHeaderChange}/>
<hr/>
<ContentView flow={flow} message={flow.request}/>
</section>
);
},
+ 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 (
<section className="response">
{/*<RequestLine flow={flow}/>*/}
- <ResponseLine flow={flow}/>
- <Headers message={flow.response} onChange={this.onHeaderChange}/>
+ <ResponseLine ref="responseLine" flow={flow}/>
+ <Headers ref="headers" message={flow.response} onChange={this.onHeaderChange}/>
<hr/>
<ContentView flow={flow} message={flow.response}/>
</section>
);
},
+ 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 52d6d0ad..229d691b 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 (
<footer>
- {mode != "regular" ? <span className="label label-success">{mode} mode</span> : null}
+ {mode && mode != "regular" ? <span className="label label-success">{mode} mode</span> : null}
&nbsp;
{intercept ? <span className="label label-success">Intercept: {intercept}</span> : null}
</footer>
diff --git a/web/src/js/components/header.js b/web/src/js/components/header.js
index 6cd1006d..225f5b9f 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} />
<FilterInput
ref="highlight"
placeholder="Highlight"
diff --git a/web/src/js/components/mainview.js b/web/src/js/components/mainview.js
index 2b7819c5..9ff51dfa 100644
--- a/web/src/js/components/mainview.js
+++ b/web/src/js/components/mainview.js
@@ -1,12 +1,13 @@
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({
@@ -105,7 +106,7 @@ var MainView = React.createClass({
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;
@@ -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 00000000..229e82c8
--- /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 <span
+ key={o.key}
+ className="option"
+ onClick={onClick}>
+ {prefix}
+ <strong className="text-primary">{o.key}</strong>{suffix}
+ </span>;
+ }.bind(this));
+ return <div tabIndex="0" onKeyDown={this.onKeyDown} onClick={this.onClick} className="prompt-dialog">
+ <div className="prompt-content">
+ {this.props.prompt || <strong>Select: </strong> }
+ {opts}
+ </div>
+ </div>;
+ }
+});
+
+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 ead6f7e8..e766d6e6 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 d848ff24..40575692 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,