web: duplicate, delete, replay flows

This commit is contained in:
Maximilian Hils 2014-12-24 01:07:57 +01:00
parent b1311faa68
commit 1c4f4c2494
11 changed files with 372 additions and 235 deletions

View File

@ -428,6 +428,11 @@ class FlowStore(FlowList):
self.views = []
self._recalculate_views()
def get(self, flow_id):
for f in self._list:
if f.id == flow_id:
return f
def __contains__(self, f):
return f in self._set

View File

@ -96,6 +96,8 @@ class Flow(stateobject.StateObject):
def copy(self):
f = copy.copy(self)
f.id = str(uuid.uuid4())
f.live = False
f.client_conn = self.client_conn.copy()
f.server_conn = self.server_conn.copy()
@ -142,6 +144,8 @@ class Flow(stateobject.StateObject):
Intercept this Flow. Processing will stop until accept_intercept is
called.
"""
if self.intercepted:
return
self.intercepted = True
master.handle_intercept(self)
@ -149,6 +153,8 @@ class Flow(stateobject.StateObject):
"""
Continue with the flow - called after an intercept().
"""
if not self.intercepted:
return
self.intercepted = False
self.reply()
master.handle_accept_intercept(self)

View File

@ -35,7 +35,7 @@ class WebFlowView(flow.FlowView):
app.ClientConnection.broadcast(
type="flows",
cmd="remove",
data=f.get_state(short=True)
data=f.id
)
def _recalculate(self, flows):

View File

@ -1,12 +1,13 @@
import os.path
import sys
import tornado.web
import tornado.websocket
import logging
import json
from .. import flow
class APIError(tornado.web.HTTPError):
pass
class IndexHandler(tornado.web.RequestHandler):
def get(self):
_ = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645
@ -36,38 +37,82 @@ class ClientConnection(WebSocketEventBroadcaster):
connections = set()
class Flows(tornado.web.RequestHandler):
class RequestHandler(tornado.web.RequestHandler):
@property
def state(self):
return self.application.master.state
@property
def master(self):
return self.application.master
@property
def flow(self):
flow_id = str(self.path_kwargs["flow_id"])
flow = self.state.flows.get(flow_id)
if flow:
return flow
else:
raise APIError(400, "Flow not found.")
def write_error(self, status_code, **kwargs):
if "exc_info" in kwargs and isinstance(kwargs["exc_info"][1], APIError):
self.finish(kwargs["exc_info"][1].log_message)
else:
super(RequestHandler, self).write_error(status_code, **kwargs)
class Flows(RequestHandler):
def get(self):
self.write(dict(
data=[f.get_state(short=True) for f in self.application.state.flows]
data=[f.get_state(short=True) for f in self.state.flows]
))
class AcceptFlows(tornado.web.RequestHandler):
class ClearAll(RequestHandler):
def post(self):
self.application.state.flows.accept_all(self.application.master)
self.state.clear()
class AcceptFlow(tornado.web.RequestHandler):
class AcceptFlows(RequestHandler):
def post(self):
self.state.flows.accept_all(self.master)
class AcceptFlow(RequestHandler):
def post(self, flow_id):
flow_id = str(flow_id)
for flow in self.application.state.flows:
if flow.id == flow_id:
flow.accept_intercept(self.application.master)
break
self.flow.accept_intercept(self.master)
class Events(tornado.web.RequestHandler):
class FlowHandler(RequestHandler):
def delete(self, flow_id):
self.flow.kill(self.master)
self.state.delete_flow(self.flow)
class DuplicateFlow(RequestHandler):
def post(self, flow_id):
self.master.duplicate_flow(self.flow)
class ReplayFlow(RequestHandler):
def post(self, flow_id):
self.flow.backup()
r = self.master.replay_request(self.flow)
if r:
raise APIError(400, r)
class Events(RequestHandler):
def get(self):
self.write(dict(
data=list(self.application.state.events)
data=list(self.state.events)
))
class Settings(tornado.web.RequestHandler):
class Settings(RequestHandler):
def get(self):
self.write(dict(
data=dict(
intercept=self.application.state.intercept_txt
intercept=self.state.intercept_txt
)
))
@ -81,7 +126,7 @@ class Settings(tornado.web.RequestHandler):
if k == "_xsrf":
continue
elif k == "intercept":
self.application.state.set_intercept(v[0])
self.state.set_intercept(v[0])
update[k] = v[0]
else:
print "Warning: Unknown setting {}: {}".format(k, v)
@ -93,17 +138,7 @@ class Settings(tornado.web.RequestHandler):
)
class Clear(tornado.web.RequestHandler):
def post(self):
self.application.state.clear()
class Application(tornado.web.Application):
@property
def state(self):
return self.master.state
def __init__(self, master, debug):
self.master = master
handlers = [
@ -112,9 +147,12 @@ class Application(tornado.web.Application):
(r"/events", Events),
(r"/flows", Flows),
(r"/flows/accept", AcceptFlows),
(r"/flows/([0-9a-f\-]+)/accept", AcceptFlow),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)", FlowHandler),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/accept", AcceptFlow),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/duplicate", DuplicateFlow),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/replay", ReplayFlow),
(r"/settings", Settings),
(r"/clear", Clear),
(r"/clear", ClearAll),
]
settings = dict(
template_path=os.path.join(os.path.dirname(__file__), "templates"),

View File

@ -1,81 +1,3 @@
// 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 Navigation = _.extend({}, ReactRouter.Navigation, {
setQuery: function (dict) {
var q = this.context.getCurrentQuery();
for(var i in dict){
if(dict.hasOwnProperty(i)){
q[i] = dict[i] || undefined; //falsey values shall be removed.
}
}
q._ = "_"; // workaround for https://github.com/rackt/react-router/pull/599
this.replaceWith(this.context.getCurrentPath(), this.context.getCurrentParams(), q);
},
replaceWith: function(routeNameOrPath, params, query) {
if(routeNameOrPath === undefined){
routeNameOrPath = this.context.getCurrentPath();
}
if(params === undefined){
params = this.context.getCurrentParams();
}
if(query === undefined){
query = this.context.getCurrentQuery();
}
ReactRouter.Navigation.replaceWith.call(this, routeNameOrPath, params, query);
}
});
_.extend(Navigation.contextTypes, ReactRouter.State.contextTypes);
var State = _.extend({}, ReactRouter.State, {
getInitialState: function () {
this._query = this.context.getCurrentQuery();
this._queryWatches = [];
return null;
},
onQueryChange: function (key, callback) {
this._queryWatches.push({
key: key,
callback: callback
});
},
componentWillReceiveProps: function (nextProps, nextState) {
var q = this.context.getCurrentQuery();
for (var i = 0; i < this._queryWatches.length; i++) {
var watch = this._queryWatches[i];
if (this._query[watch.key] !== q[watch.key]) {
watch.callback(this._query[watch.key], q[watch.key], watch.key);
}
}
this._query = q;
}
});
var Key = {
UP: 38,
DOWN: 40,
@ -92,7 +14,7 @@ var Key = {
BACKSPACE: 8,
};
// Add A-Z
for(var i=65; i <= 90; i++){
for (var i = 65; i <= 90; i++) {
Key[String.fromCharCode(i)] = i;
}
@ -157,6 +79,31 @@ EventEmitter.prototype.removeListener = function (events, f) {
}
}.bind(this));
};
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
var xsrf = $.param({_xsrf: getCookie("_xsrf")});
//Tornado XSRF Protection.
jQuery.ajaxPrefilter(function (options) {
if (["post", "put", "delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") {
if (options.data) {
options.data += ("&" + xsrf);
} else {
options.data = xsrf;
}
}
});
// Log AJAX Errors
$(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {
var message = jqXHR.responseText;
console.error(message, arguments);
EventLogActions.add_event(thrownError + ": " + message);
window.alert(message);
});
const PayloadSources = {
VIEW: "view",
SERVER: "server"
@ -272,7 +219,18 @@ var FlowActions = {
accept_all: function(){
jQuery.post("/flows/accept");
},
"delete": function(flow){
jQuery.ajax({
type:"DELETE",
url: "/flows/" + flow.id
});
},
duplicate: function(flow){
jQuery.post("/flows/" + flow.id + "/duplicate");
},
replay: function(flow){
jQuery.post("/flows/" + flow.id + "/replay");
},
update: function (flow) {
AppDispatcher.dispatchViewAction({
type: ActionTypes.FLOW_STORE,
@ -2140,7 +2098,7 @@ _.extend(ListStore.prototype, EventEmitter.prototype, {
this.emit("update", elem);
},
remove: function (elem_id) {
if (!(elem.id in this._pos_map)) {
if (!(elem_id in this._pos_map)) {
return;
}
this.list.splice(this._pos_map[elem_id], 1);
@ -2317,8 +2275,8 @@ _.extend(StoreView.prototype, EventEmitter.prototype, {
this.store.removeListener("update", this.update);
this.store.removeListener("remove", this.remove);
this.store.removeListener("recalculate", this.recalculate);
},
recalculate: function (filt, sortfun) {
},
recalculate: function (filt, sortfun) {
if (filt) {
this.filt = filt.bind(this);
}
@ -2408,6 +2366,84 @@ function Connection(url) {
}
//React utils. For other utilities, see ../utils.js
// 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 Navigation = _.extend({}, ReactRouter.Navigation, {
setQuery: function (dict) {
var q = this.context.getCurrentQuery();
for(var i in dict){
if(dict.hasOwnProperty(i)){
q[i] = dict[i] || undefined; //falsey values shall be removed.
}
}
q._ = "_"; // workaround for https://github.com/rackt/react-router/pull/599
this.replaceWith(this.context.getCurrentPath(), this.context.getCurrentParams(), q);
},
replaceWith: function(routeNameOrPath, params, query) {
if(routeNameOrPath === undefined){
routeNameOrPath = this.context.getCurrentPath();
}
if(params === undefined){
params = this.context.getCurrentParams();
}
if(query === undefined){
query = this.context.getCurrentQuery();
}
ReactRouter.Navigation.replaceWith.call(this, routeNameOrPath, params, query);
}
});
_.extend(Navigation.contextTypes, ReactRouter.State.contextTypes);
var State = _.extend({}, ReactRouter.State, {
getInitialState: function () {
this._query = this.context.getCurrentQuery();
this._queryWatches = [];
return null;
},
onQueryChange: function (key, callback) {
this._queryWatches.push({
key: key,
callback: callback
});
},
componentWillReceiveProps: function (nextProps, nextState) {
var q = this.context.getCurrentQuery();
for (var i = 0; i < this._queryWatches.length; i++) {
var watch = this._queryWatches[i];
if (this._query[watch.key] !== q[watch.key]) {
watch.callback(this._query[watch.key], q[watch.key], watch.key);
}
}
this._query = q;
}
});
var Splitter = React.createClass({displayName: 'Splitter',
getDefaultProps: function () {
return {
@ -2512,23 +2548,6 @@ var Splitter = React.createClass({displayName: 'Splitter',
);
}
});
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
var xsrf = $.param({_xsrf: getCookie("_xsrf")});
//Tornado XSRF Protection.
$.ajaxPrefilter(function (options) {
if (["post","put","delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") {
if (options.data) {
options.data += ("&" + xsrf);
} else {
options.data = xsrf;
}
}
});
var VirtualScrollMixin = {
getInitialState: function () {
return {
@ -3686,6 +3705,10 @@ var MainView = React.createClass({displayName: 'MainView',
this.selectFlow(flows[index]);
},
onKeyDown: function (e) {
var flow = this.getSelected();
if(e.ctrlKey){
return;
}
switch (e.keyCode) {
case Key.K:
case Key.UP:
@ -3724,11 +3747,32 @@ var MainView = React.createClass({displayName: 'MainView',
this.refs.flowDetails.nextTab(+1);
}
break;
case Key.C:
if (e.shiftKey) {
FlowActions.clear();
}
break;
case Key.D:
if (flow) {
if (e.shiftKey) {
FlowActions.duplicate(flow);
} else {
var last_flow = this.state.view.index(flow) === this.state.view.list.length - 1;
this.selectFlowRelative(last_flow ? -1 : +1);
FlowActions.delete(flow);
}
}
break;
case Key.A:
if (e.shiftKey) {
FlowActions.accept_all();
} else if(this.getSelected()) {
FlowActions.accept(this.getSelected());
} else if (flow) {
FlowActions.accept(flow);
}
break;
case Key.R:
if(!e.shiftKey && flow){
FlowActions.replay(flow);
}
break;
default:

View File

@ -77,7 +77,18 @@ var FlowActions = {
accept_all: function(){
jQuery.post("/flows/accept");
},
"delete": function(flow){
jQuery.ajax({
type:"DELETE",
url: "/flows/" + flow.id
});
},
duplicate: function(flow){
jQuery.post("/flows/" + flow.id + "/duplicate");
},
replay: function(flow){
jQuery.post("/flows/" + flow.id + "/replay");
},
update: function (flow) {
AppDispatcher.dispatchViewAction({
type: ActionTypes.FLOW_STORE,

View File

@ -105,6 +105,10 @@ var MainView = React.createClass({
this.selectFlow(flows[index]);
},
onKeyDown: function (e) {
var flow = this.getSelected();
if(e.ctrlKey){
return;
}
switch (e.keyCode) {
case Key.K:
case Key.UP:
@ -143,11 +147,32 @@ var MainView = React.createClass({
this.refs.flowDetails.nextTab(+1);
}
break;
case Key.C:
if (e.shiftKey) {
FlowActions.clear();
}
break;
case Key.D:
if (flow) {
if (e.shiftKey) {
FlowActions.duplicate(flow);
} else {
var last_flow = this.state.view.index(flow) === this.state.view.list.length - 1;
this.selectFlowRelative(last_flow ? -1 : +1);
FlowActions.delete(flow);
}
}
break;
case Key.A:
if (e.shiftKey) {
FlowActions.accept_all();
} else if(this.getSelected()) {
FlowActions.accept(this.getSelected());
} else if (flow) {
FlowActions.accept(flow);
}
break;
case Key.R:
if(!e.shiftKey && flow){
FlowActions.replay(flow);
}
break;
default:

View File

@ -1,5 +1,83 @@
//React utils. For other utilities, see ../utils.js
// 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 Navigation = _.extend({}, ReactRouter.Navigation, {
setQuery: function (dict) {
var q = this.context.getCurrentQuery();
for(var i in dict){
if(dict.hasOwnProperty(i)){
q[i] = dict[i] || undefined; //falsey values shall be removed.
}
}
q._ = "_"; // workaround for https://github.com/rackt/react-router/pull/599
this.replaceWith(this.context.getCurrentPath(), this.context.getCurrentParams(), q);
},
replaceWith: function(routeNameOrPath, params, query) {
if(routeNameOrPath === undefined){
routeNameOrPath = this.context.getCurrentPath();
}
if(params === undefined){
params = this.context.getCurrentParams();
}
if(query === undefined){
query = this.context.getCurrentQuery();
}
ReactRouter.Navigation.replaceWith.call(this, routeNameOrPath, params, query);
}
});
_.extend(Navigation.contextTypes, ReactRouter.State.contextTypes);
var State = _.extend({}, ReactRouter.State, {
getInitialState: function () {
this._query = this.context.getCurrentQuery();
this._queryWatches = [];
return null;
},
onQueryChange: function (key, callback) {
this._queryWatches.push({
key: key,
callback: callback
});
},
componentWillReceiveProps: function (nextProps, nextState) {
var q = this.context.getCurrentQuery();
for (var i = 0; i < this._queryWatches.length; i++) {
var watch = this._queryWatches[i];
if (this._query[watch.key] !== q[watch.key]) {
watch.callback(this._query[watch.key], q[watch.key], watch.key);
}
}
this._query = q;
}
});
var Splitter = React.createClass({
getDefaultProps: function () {
return {
@ -103,21 +181,4 @@ var Splitter = React.createClass({
</div>
);
}
});
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
var xsrf = $.param({_xsrf: getCookie("_xsrf")});
//Tornado XSRF Protection.
$.ajaxPrefilter(function (options) {
if (["post","put","delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") {
if (options.data) {
options.data += ("&" + xsrf);
} else {
options.data = xsrf;
}
}
});

View File

@ -19,7 +19,7 @@ _.extend(ListStore.prototype, EventEmitter.prototype, {
this.emit("update", elem);
},
remove: function (elem_id) {
if (!(elem.id in this._pos_map)) {
if (!(elem_id in this._pos_map)) {
return;
}
this.list.splice(this._pos_map[elem_id], 1);

View File

@ -32,8 +32,8 @@ _.extend(StoreView.prototype, EventEmitter.prototype, {
this.store.removeListener("update", this.update);
this.store.removeListener("remove", this.remove);
this.store.removeListener("recalculate", this.recalculate);
},
recalculate: function (filt, sortfun) {
},
recalculate: function (filt, sortfun) {
if (filt) {
this.filt = filt.bind(this);
}

View File

@ -1,81 +1,3 @@
// 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 Navigation = _.extend({}, ReactRouter.Navigation, {
setQuery: function (dict) {
var q = this.context.getCurrentQuery();
for(var i in dict){
if(dict.hasOwnProperty(i)){
q[i] = dict[i] || undefined; //falsey values shall be removed.
}
}
q._ = "_"; // workaround for https://github.com/rackt/react-router/pull/599
this.replaceWith(this.context.getCurrentPath(), this.context.getCurrentParams(), q);
},
replaceWith: function(routeNameOrPath, params, query) {
if(routeNameOrPath === undefined){
routeNameOrPath = this.context.getCurrentPath();
}
if(params === undefined){
params = this.context.getCurrentParams();
}
if(query === undefined){
query = this.context.getCurrentQuery();
}
ReactRouter.Navigation.replaceWith.call(this, routeNameOrPath, params, query);
}
});
_.extend(Navigation.contextTypes, ReactRouter.State.contextTypes);
var State = _.extend({}, ReactRouter.State, {
getInitialState: function () {
this._query = this.context.getCurrentQuery();
this._queryWatches = [];
return null;
},
onQueryChange: function (key, callback) {
this._queryWatches.push({
key: key,
callback: callback
});
},
componentWillReceiveProps: function (nextProps, nextState) {
var q = this.context.getCurrentQuery();
for (var i = 0; i < this._queryWatches.length; i++) {
var watch = this._queryWatches[i];
if (this._query[watch.key] !== q[watch.key]) {
watch.callback(this._query[watch.key], q[watch.key], watch.key);
}
}
this._query = q;
}
});
var Key = {
UP: 38,
DOWN: 40,
@ -92,7 +14,7 @@ var Key = {
BACKSPACE: 8,
};
// Add A-Z
for(var i=65; i <= 90; i++){
for (var i = 65; i <= 90; i++) {
Key[String.fromCharCode(i)] = i;
}
@ -156,4 +78,29 @@ EventEmitter.prototype.removeListener = function (events, f) {
this.listeners[event].splice(index, 1);
}
}.bind(this));
};
};
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
var xsrf = $.param({_xsrf: getCookie("_xsrf")});
//Tornado XSRF Protection.
jQuery.ajaxPrefilter(function (options) {
if (["post", "put", "delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") {
if (options.data) {
options.data += ("&" + xsrf);
} else {
options.data = xsrf;
}
}
});
// Log AJAX Errors
$(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {
var message = jqXHR.responseText;
console.error(message, arguments);
EventLogActions.add_event(thrownError + ": " + message);
window.alert(message);
});