web: add prompt for keyboard navigation

This commit is contained in:
Maximilian Hils
2015-03-30 03:49:50 +02:00
parent 737002921e
commit 6d29f93e9e
14 changed files with 599 additions and 132 deletions

View File

@@ -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";

27
web/src/css/prompt.less Normal file
View File

@@ -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: ", ";
}
}
}

View File

@@ -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);
});

View File

@@ -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>;
}
});

View File

@@ -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>
);
}

View File

@@ -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: {

View File

@@ -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>

View File

@@ -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"

View File

@@ -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);

View File

@@ -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;

View File

@@ -72,7 +72,7 @@ var ProxyAppMain = React.createClass({
selectFilterInput("intercept");
break;
case Key.L:
selectFilterInput("filter");
selectFilterInput("search");
break;
case Key.H:
selectFilterInput("highlight");

View File

@@ -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,