mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-26 02:10:59 +00:00
flowtable: add selection indicator, add keyboard navigation
This commit is contained in:
parent
6a161be6b4
commit
0d64cc9327
@ -1,5 +1,6 @@
|
||||
from __future__ import absolute_import
|
||||
import copy
|
||||
import uuid
|
||||
import netlib.tcp
|
||||
from .. import stateobject, utils, version
|
||||
from ..proxy.connection import ClientConnection, ServerConnection
|
||||
@ -60,6 +61,7 @@ class Flow(stateobject.StateObject):
|
||||
"""
|
||||
def __init__(self, conntype, client_conn, server_conn, live=None):
|
||||
self.conntype = conntype
|
||||
self.id = str(uuid.uuid4())
|
||||
self.client_conn = client_conn
|
||||
"""@type: ClientConnection"""
|
||||
self.server_conn = server_conn
|
||||
@ -72,6 +74,7 @@ class Flow(stateobject.StateObject):
|
||||
self._backup = None
|
||||
|
||||
_stateobject_attributes = dict(
|
||||
id=str,
|
||||
error=Error,
|
||||
client_conn=ClientConnection,
|
||||
server_conn=ServerConnection,
|
||||
|
@ -56,7 +56,7 @@ body,
|
||||
#container > .eventlog {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
#main {
|
||||
main {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
@ -111,6 +111,9 @@ header .menu {
|
||||
.flow-table tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
.flow-table tr.selected {
|
||||
background-color: rgba(193, 215, 235, 0.5) !important;
|
||||
}
|
||||
.flow-table td {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
@ -119,7 +122,7 @@ header .menu {
|
||||
.flow-table tr:nth-child(even) {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.flow-table .col-tls {
|
||||
.flow-table .col-tls {
|
||||
width: 10px;
|
||||
}
|
||||
.flow-table .col-tls-https {
|
||||
|
@ -1,4 +1,5 @@
|
||||
[{
|
||||
"id": "b5e5483c-e124-45bb-aa2e-360706e03ef4",
|
||||
"request": {
|
||||
"timestamp_end": 1410651311.107,
|
||||
"timestamp_start": 1410651311.106,
|
||||
@ -157,6 +158,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "85e9781f-d81d-43ca-a694-2cd86c76d991",
|
||||
"request": {
|
||||
"timestamp_end": 1410651311.657,
|
||||
"timestamp_start": 1410651311.653,
|
||||
@ -319,6 +321,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "1bf281fd-e02a-423c-a69c-aa65657bc3dd",
|
||||
"request": {
|
||||
"timestamp_end": 1410651312.362,
|
||||
"timestamp_start": 1410651312.359,
|
||||
@ -485,6 +488,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "833253a0-f7dd-48c7-893c-1f13a38a71ce",
|
||||
"request": {
|
||||
"timestamp_end": 1410651312.389,
|
||||
"timestamp_start": 1410651312.368,
|
||||
@ -651,6 +655,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "152d8e71-2469-4034-8d6d-11099bbb4248",
|
||||
"request": {
|
||||
"timestamp_end": 1410651312.386,
|
||||
"timestamp_start": 1410651312.368,
|
||||
@ -817,6 +822,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "b3758e4d-7bae-4771-b154-e100c0722d00",
|
||||
"request": {
|
||||
"timestamp_end": 1410651373.965,
|
||||
"timestamp_start": 1410651373.963,
|
||||
@ -947,6 +953,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ea9e47ab-fd7b-4463-bfea-cfd64cc5f78d",
|
||||
"request": {
|
||||
"timestamp_end": 1410651374.391,
|
||||
"timestamp_start": 1410651374.387,
|
||||
@ -1081,6 +1088,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "13ee4cd1-08e0-43ef-9bee-56fc0d9cbf3f",
|
||||
"request": {
|
||||
"timestamp_end": 1410651374.396,
|
||||
"timestamp_start": 1410651374.394,
|
||||
@ -1211,7 +1219,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"id": "5c50e1fc-5ac4-4748-aed1-c969ede63e4e",
|
||||
"request": {
|
||||
"timestamp_end": 1410651374.795,
|
||||
"timestamp_start": 1410651374.793,
|
||||
"form_in": "absolute",
|
||||
@ -1361,7 +1370,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"id": "0285a0b2-380e-43eb-a7a9-a18893950216",
|
||||
"request": {
|
||||
"timestamp_end": 1410651375.084,
|
||||
"timestamp_start": 1410651375.078,
|
||||
"form_in": "absolute",
|
||||
@ -1507,7 +1517,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"id": "c9af9c71-dc68-462e-8446-f3a4b2782400",
|
||||
"request": {
|
||||
"timestamp_end": 1410651374.778,
|
||||
"timestamp_start": 1410651374.766,
|
||||
"form_in": "absolute",
|
||||
@ -1637,7 +1648,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"id": "310386ab-3ae1-4129-9a2e-8dd2ce60ecdb",
|
||||
"request": {
|
||||
"timestamp_end": 1410651374.778,
|
||||
"timestamp_start": 1410651374.766,
|
||||
"form_in": "absolute",
|
||||
@ -1767,7 +1779,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"id": "b92e5f6e-bb0f-4e47-a50c-ef4072ea40b3",
|
||||
"request": {
|
||||
"timestamp_end": 1410651376.078,
|
||||
"timestamp_start": 1410651376.075,
|
||||
"form_in": "absolute",
|
||||
@ -1889,7 +1902,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"id": "597d086f-d836-49e3-85bb-77a983bed87f",
|
||||
"request": {
|
||||
"timestamp_end": 1410651376.282,
|
||||
"timestamp_start": 1410651376.279,
|
||||
"form_in": "absolute",
|
||||
|
@ -12,6 +12,15 @@ var AutoScrollMixin = {
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
var Key = {
|
||||
UP: 38,
|
||||
DOWN: 40,
|
||||
LEFT: 37,
|
||||
RIGHT: 39,
|
||||
ENTER: 13,
|
||||
ESC: 27
|
||||
}
|
||||
const PayloadSources = {
|
||||
VIEW: "view",
|
||||
SERVER: "server"
|
||||
@ -274,9 +283,8 @@ _.extend(FlowView.prototype, EventEmitter.prototype, {
|
||||
this.emit("change");
|
||||
},
|
||||
_update: function(flow){
|
||||
console.debug("FIXME: Use UUID");
|
||||
var idx = _.findIndex(this.flows, function(f){
|
||||
return flow.request.timestamp_start == f.request.timestamp_start;
|
||||
return flow.id === f.id;
|
||||
});
|
||||
|
||||
if(idx < 0){
|
||||
@ -300,7 +308,13 @@ _.extend(_FlowStore.prototype, EventEmitter.prototype, {
|
||||
var view = new FlowView(this, !since);
|
||||
|
||||
$.getJSON("/static/flows.json", function(flows){
|
||||
flows = flows.concat(_.cloneDeep(flows)).concat(_.cloneDeep(flows));
|
||||
var id = 1;
|
||||
flows.forEach(function(flow){
|
||||
flow.id = "uuid-"+id++;
|
||||
})
|
||||
view.add_bulk(flows);
|
||||
|
||||
});
|
||||
|
||||
return view;
|
||||
@ -447,38 +461,6 @@ var Header = React.createClass({displayName: 'Header',
|
||||
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var FlowRow = React.createClass({displayName: 'FlowRow',
|
||||
render: function(){
|
||||
var flow = this.props.flow;
|
||||
var columns = this.props.columns.map(function(column){
|
||||
return column({
|
||||
key: column.displayName,
|
||||
flow: flow
|
||||
});
|
||||
}.bind(this));
|
||||
return React.DOM.tr({onClick: this.props.onClick}, columns);
|
||||
}
|
||||
});
|
||||
|
||||
var FlowTableHead = React.createClass({displayName: 'FlowTableHead',
|
||||
render: function(){
|
||||
var columns = this.props.columns.map(function(column){
|
||||
return column.renderTitle();
|
||||
}.bind(this));
|
||||
return React.DOM.thead(null, columns);
|
||||
}
|
||||
});
|
||||
|
||||
var FlowTableBody = React.createClass({displayName: 'FlowTableBody',
|
||||
render: function(){
|
||||
var rows = this.props.flows.map(function(flow){
|
||||
//TODO: Add UUID
|
||||
return FlowRow({onClick: this.props.onClick, flow: flow, columns: this.props.columns});
|
||||
}.bind(this));
|
||||
return React.DOM.tbody(null, rows);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var TLSColumn = React.createClass({displayName: 'TLSColumn',
|
||||
statics: {
|
||||
@ -573,6 +555,54 @@ var TimeColumn = React.createClass({displayName: 'TimeColumn',
|
||||
var all_columns = [TLSColumn, IconColumn, PathColumn, MethodColumn, StatusColumn, TimeColumn];
|
||||
|
||||
|
||||
/** @jsx React.DOM */
|
||||
|
||||
var FlowRow = React.createClass({displayName: 'FlowRow',
|
||||
render: function(){
|
||||
var flow = this.props.flow;
|
||||
var columns = this.props.columns.map(function(column){
|
||||
return column({
|
||||
key: column.displayName,
|
||||
flow: flow
|
||||
});
|
||||
}.bind(this));
|
||||
var className = "";
|
||||
if(this.props.selected){
|
||||
className += "selected";
|
||||
}
|
||||
return (
|
||||
React.DOM.tr({className: className, onClick: this.props.selectFlow.bind(null, flow)},
|
||||
columns
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
var FlowTableHead = React.createClass({displayName: 'FlowTableHead',
|
||||
render: function(){
|
||||
var columns = this.props.columns.map(function(column){
|
||||
return column.renderTitle();
|
||||
}.bind(this));
|
||||
return React.DOM.thead(null, columns);
|
||||
}
|
||||
});
|
||||
|
||||
var FlowTableBody = React.createClass({displayName: 'FlowTableBody',
|
||||
render: function(){
|
||||
var rows = this.props.flows.map(function(flow){
|
||||
var selected = (flow == this.props.selected);
|
||||
return FlowRow({key: flow.id,
|
||||
ref: flow.id,
|
||||
flow: flow,
|
||||
columns: this.props.columns,
|
||||
selected: selected,
|
||||
selectFlow: this.props.selectFlow}
|
||||
);
|
||||
}.bind(this));
|
||||
return React.DOM.tbody({onKeyDown: this.props.onKeyDown, tabIndex: "0"}, rows);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var FlowTable = React.createClass({displayName: 'FlowTable',
|
||||
getInitialState: function () {
|
||||
return {
|
||||
@ -593,18 +623,88 @@ var FlowTable = React.createClass({displayName: 'FlowTable',
|
||||
flows: this.flowStore.getAll()
|
||||
});
|
||||
},
|
||||
onClick: function(e){
|
||||
console.log("rowclick", e);
|
||||
selectFlow: function(flow){
|
||||
this.setState({
|
||||
selected: flow
|
||||
});
|
||||
|
||||
// Now comes the fun part: Scroll the flow into the view.
|
||||
var viewport = this.getDOMNode();
|
||||
var flowNode = this.refs.body.refs[flow.id].getDOMNode();
|
||||
var viewport_top = viewport.scrollTop;
|
||||
var viewport_bottom = viewport_top + viewport.offsetHeight;
|
||||
var flowNode_top = flowNode.offsetTop;
|
||||
var flowNode_bottom = flowNode_top + flowNode.offsetHeight;
|
||||
|
||||
// Account for pinned thead by pretending that the flowNode starts
|
||||
// -thead_height pixel earlier.
|
||||
flowNode_top -= this.refs.body.getDOMNode().offsetTop;
|
||||
|
||||
if(flowNode_top < viewport_top){
|
||||
viewport.scrollTop = flowNode_top;
|
||||
} else if(flowNode_bottom > viewport_bottom) {
|
||||
viewport.scrollTop = flowNode_bottom - viewport.offsetHeight;
|
||||
}
|
||||
},
|
||||
selectRowRelative: function(i){
|
||||
var index;
|
||||
if(!this.state.selected){
|
||||
if(i > 0){
|
||||
index = this.flows.length-1;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
} else {
|
||||
index = _.findIndex(this.state.flows, function(f){
|
||||
return f === this.state.selected;
|
||||
}.bind(this));
|
||||
index = Math.min(Math.max(0, index+i), this.state.flows.length-1);
|
||||
}
|
||||
this.selectFlow(this.state.flows[index]);
|
||||
},
|
||||
onKeyDown: function(e){
|
||||
switch(e.keyCode){
|
||||
case Key.DOWN:
|
||||
this.selectRowRelative(+1);
|
||||
return false;
|
||||
break;
|
||||
case Key.UP:
|
||||
this.selectRowRelative(-1);
|
||||
return false;
|
||||
break;
|
||||
case Key.ENTER:
|
||||
console.log("Open details pane...", this.state.selected);
|
||||
break;
|
||||
case Key.ESC:
|
||||
console.log("")
|
||||
default:
|
||||
console.debug("keydown", e.keyCode);
|
||||
return;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onScroll: function(e){
|
||||
//Abusing CSS transforms to set thead into position:fixed.
|
||||
var head = this.refs.head.getDOMNode();
|
||||
head.style.transform = "translate(0,"+this.getDOMNode().scrollTop+"px)";
|
||||
},
|
||||
render: function () {
|
||||
var flows = this.state.flows.map(function(flow){
|
||||
return React.DOM.div(null, flow.request.method, " ", flow.request.scheme, "://", flow.request.host, flow.request.path);
|
||||
});
|
||||
return (
|
||||
React.DOM.main({onScroll: this.onScroll},
|
||||
React.DOM.table({className: "flow-table"},
|
||||
FlowTableHead({columns: this.state.columns}),
|
||||
FlowTableBody({onClick: this.onClick, columns: this.state.columns, flows: this.state.flows})
|
||||
FlowTableHead({ref: "head",
|
||||
columns: this.state.columns}),
|
||||
FlowTableBody({ref: "body",
|
||||
selectFlow: this.selectFlow,
|
||||
onKeyDown: this.onKeyDown,
|
||||
selected: this.state.selected,
|
||||
columns: this.state.columns,
|
||||
flows: this.state.flows})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -691,7 +791,7 @@ var ProxyAppMain = React.createClass({displayName: 'ProxyAppMain',
|
||||
return (
|
||||
React.DOM.div({id: "container"},
|
||||
Header({settings: this.state.settings}),
|
||||
React.DOM.div({id: "main"}, this.props.activeRouteHandler(null)),
|
||||
this.props.activeRouteHandler(null),
|
||||
this.state.settings.showEventLog ? EventLog(null) : null,
|
||||
Footer({settings: this.state.settings})
|
||||
)
|
||||
|
@ -42,6 +42,7 @@ var path = {
|
||||
'js/stores/flowstore.js',
|
||||
'js/connection.js',
|
||||
'js/components/header.jsx.js',
|
||||
'js/components/flowtable-columns.jsx.js',
|
||||
'js/components/flowtable.jsx.js',
|
||||
'js/components/eventlog.jsx.js',
|
||||
'js/components/footer.jsx.js',
|
||||
|
@ -8,6 +8,9 @@
|
||||
|
||||
tr {
|
||||
cursor: pointer;
|
||||
&.selected {
|
||||
background-color: hsla(209, 52%, 84%, 0.5) !important;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
@ -19,8 +22,6 @@
|
||||
//tr:nth-child(odd) { background-color : white; }
|
||||
tr:nth-child(even) { background-color : rgba(0,0,0,0.05); }
|
||||
//tr:hover { background-color : hsla(209, 52%, 84%, 0.5); }
|
||||
|
||||
|
||||
|
||||
.col-tls {
|
||||
width: 10px;
|
||||
|
@ -13,7 +13,7 @@ html, body, #container {
|
||||
}
|
||||
}
|
||||
|
||||
#main {
|
||||
main {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
95
web/src/js/components/flowtable-columns.jsx.js
Normal file
95
web/src/js/components/flowtable-columns.jsx.js
Normal file
@ -0,0 +1,95 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
|
||||
var TLSColumn = React.createClass({
|
||||
statics: {
|
||||
renderTitle: function(){
|
||||
return <th key="tls" className="col-tls"></th>;
|
||||
}
|
||||
},
|
||||
render: function(){
|
||||
var flow = this.props.flow;
|
||||
var ssl = (flow.request.scheme == "https");
|
||||
return <td className={ssl ? "col-tls-https" : "col-tls-http"}></td>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var IconColumn = React.createClass({
|
||||
statics: {
|
||||
renderTitle: function(){
|
||||
return <th key="icon" className="col-icon"></th>;
|
||||
}
|
||||
},
|
||||
render: function(){
|
||||
var flow = this.props.flow;
|
||||
return <td className="resource-icon resource-icon-plain"></td>;
|
||||
}
|
||||
});
|
||||
|
||||
var PathColumn = React.createClass({
|
||||
statics: {
|
||||
renderTitle: function(){
|
||||
return <th key="path" className="col-path">Path</th>;
|
||||
}
|
||||
},
|
||||
render: function(){
|
||||
var flow = this.props.flow;
|
||||
return <td>{flow.request.scheme + "://" + flow.request.host + flow.request.path}</td>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var MethodColumn = React.createClass({
|
||||
statics: {
|
||||
renderTitle: function(){
|
||||
return <th key="method" className="col-method">Method</th>;
|
||||
}
|
||||
},
|
||||
render: function(){
|
||||
var flow = this.props.flow;
|
||||
return <td>{flow.request.method}</td>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var StatusColumn = React.createClass({
|
||||
statics: {
|
||||
renderTitle: function(){
|
||||
return <th key="status" className="col-status">Status</th>;
|
||||
}
|
||||
},
|
||||
render: function(){
|
||||
var flow = this.props.flow;
|
||||
var status;
|
||||
if(flow.response){
|
||||
status = flow.response.code;
|
||||
} else {
|
||||
status = null;
|
||||
}
|
||||
return <td>{status}</td>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var TimeColumn = React.createClass({
|
||||
statics: {
|
||||
renderTitle: function(){
|
||||
return <th key="time" className="col-time">Time</th>;
|
||||
}
|
||||
},
|
||||
render: function(){
|
||||
var flow = this.props.flow;
|
||||
var time;
|
||||
if(flow.response){
|
||||
time = Math.round(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))+"ms";
|
||||
} else {
|
||||
time = "...";
|
||||
}
|
||||
return <td>{time}</td>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var all_columns = [TLSColumn, IconColumn, PathColumn, MethodColumn, StatusColumn, TimeColumn];
|
||||
|
@ -9,7 +9,14 @@ var FlowRow = React.createClass({
|
||||
flow: flow
|
||||
});
|
||||
}.bind(this));
|
||||
return <tr onClick={this.props.onClick} >{columns}</tr>;
|
||||
var className = "";
|
||||
if(this.props.selected){
|
||||
className += "selected";
|
||||
}
|
||||
return (
|
||||
<tr className={className} onClick={this.props.selectFlow.bind(null, flow)}>
|
||||
{columns}
|
||||
</tr>);
|
||||
}
|
||||
});
|
||||
|
||||
@ -25,107 +32,20 @@ var FlowTableHead = React.createClass({
|
||||
var FlowTableBody = React.createClass({
|
||||
render: function(){
|
||||
var rows = this.props.flows.map(function(flow){
|
||||
//TODO: Add UUID
|
||||
return <FlowRow onClick={this.props.onClick} flow={flow} columns={this.props.columns}/>;
|
||||
var selected = (flow == this.props.selected);
|
||||
return <FlowRow key={flow.id}
|
||||
ref={flow.id}
|
||||
flow={flow}
|
||||
columns={this.props.columns}
|
||||
selected={selected}
|
||||
selectFlow={this.props.selectFlow}
|
||||
/>;
|
||||
}.bind(this));
|
||||
return <tbody>{rows}</tbody>;
|
||||
return <tbody onKeyDown={this.props.onKeyDown} tabIndex="0">{rows}</tbody>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var TLSColumn = React.createClass({
|
||||
statics: {
|
||||
renderTitle: function(){
|
||||
return <th key="tls" className="col-tls"></th>;
|
||||
}
|
||||
},
|
||||
render: function(){
|
||||
var flow = this.props.flow;
|
||||
var ssl = (flow.request.scheme == "https");
|
||||
return <td className={ssl ? "col-tls-https" : "col-tls-http"}></td>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var IconColumn = React.createClass({
|
||||
statics: {
|
||||
renderTitle: function(){
|
||||
return <th key="icon" className="col-icon"></th>;
|
||||
}
|
||||
},
|
||||
render: function(){
|
||||
var flow = this.props.flow;
|
||||
return <td className="resource-icon resource-icon-plain"></td>;
|
||||
}
|
||||
});
|
||||
|
||||
var PathColumn = React.createClass({
|
||||
statics: {
|
||||
renderTitle: function(){
|
||||
return <th key="path" className="col-path">Path</th>;
|
||||
}
|
||||
},
|
||||
render: function(){
|
||||
var flow = this.props.flow;
|
||||
return <td>{flow.request.scheme + "://" + flow.request.host + flow.request.path}</td>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var MethodColumn = React.createClass({
|
||||
statics: {
|
||||
renderTitle: function(){
|
||||
return <th key="method" className="col-method">Method</th>;
|
||||
}
|
||||
},
|
||||
render: function(){
|
||||
var flow = this.props.flow;
|
||||
return <td>{flow.request.method}</td>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var StatusColumn = React.createClass({
|
||||
statics: {
|
||||
renderTitle: function(){
|
||||
return <th key="status" className="col-status">Status</th>;
|
||||
}
|
||||
},
|
||||
render: function(){
|
||||
var flow = this.props.flow;
|
||||
var status;
|
||||
if(flow.response){
|
||||
status = flow.response.code;
|
||||
} else {
|
||||
status = null;
|
||||
}
|
||||
return <td>{status}</td>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var TimeColumn = React.createClass({
|
||||
statics: {
|
||||
renderTitle: function(){
|
||||
return <th key="time" className="col-time">Time</th>;
|
||||
}
|
||||
},
|
||||
render: function(){
|
||||
var flow = this.props.flow;
|
||||
var time;
|
||||
if(flow.response){
|
||||
time = Math.round(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))+"ms";
|
||||
} else {
|
||||
time = "...";
|
||||
}
|
||||
return <td>{time}</td>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var all_columns = [TLSColumn, IconColumn, PathColumn, MethodColumn, StatusColumn, TimeColumn];
|
||||
|
||||
|
||||
var FlowTable = React.createClass({
|
||||
getInitialState: function () {
|
||||
return {
|
||||
@ -146,18 +66,88 @@ var FlowTable = React.createClass({
|
||||
flows: this.flowStore.getAll()
|
||||
});
|
||||
},
|
||||
onClick: function(e){
|
||||
console.log("rowclick", e);
|
||||
selectFlow: function(flow){
|
||||
this.setState({
|
||||
selected: flow
|
||||
});
|
||||
|
||||
// Now comes the fun part: Scroll the flow into the view.
|
||||
var viewport = this.getDOMNode();
|
||||
var flowNode = this.refs.body.refs[flow.id].getDOMNode();
|
||||
var viewport_top = viewport.scrollTop;
|
||||
var viewport_bottom = viewport_top + viewport.offsetHeight;
|
||||
var flowNode_top = flowNode.offsetTop;
|
||||
var flowNode_bottom = flowNode_top + flowNode.offsetHeight;
|
||||
|
||||
// Account for pinned thead by pretending that the flowNode starts
|
||||
// -thead_height pixel earlier.
|
||||
flowNode_top -= this.refs.body.getDOMNode().offsetTop;
|
||||
|
||||
if(flowNode_top < viewport_top){
|
||||
viewport.scrollTop = flowNode_top;
|
||||
} else if(flowNode_bottom > viewport_bottom) {
|
||||
viewport.scrollTop = flowNode_bottom - viewport.offsetHeight;
|
||||
}
|
||||
},
|
||||
selectRowRelative: function(i){
|
||||
var index;
|
||||
if(!this.state.selected){
|
||||
if(i > 0){
|
||||
index = this.flows.length-1;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
} else {
|
||||
index = _.findIndex(this.state.flows, function(f){
|
||||
return f === this.state.selected;
|
||||
}.bind(this));
|
||||
index = Math.min(Math.max(0, index+i), this.state.flows.length-1);
|
||||
}
|
||||
this.selectFlow(this.state.flows[index]);
|
||||
},
|
||||
onKeyDown: function(e){
|
||||
switch(e.keyCode){
|
||||
case Key.DOWN:
|
||||
this.selectRowRelative(+1);
|
||||
return false;
|
||||
break;
|
||||
case Key.UP:
|
||||
this.selectRowRelative(-1);
|
||||
return false;
|
||||
break;
|
||||
case Key.ENTER:
|
||||
console.log("Open details pane...", this.state.selected);
|
||||
break;
|
||||
case Key.ESC:
|
||||
console.log("")
|
||||
default:
|
||||
console.debug("keydown", e.keyCode);
|
||||
return;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onScroll: function(e){
|
||||
//Abusing CSS transforms to set thead into position:fixed.
|
||||
var head = this.refs.head.getDOMNode();
|
||||
head.style.transform = "translate(0,"+this.getDOMNode().scrollTop+"px)";
|
||||
},
|
||||
render: function () {
|
||||
var flows = this.state.flows.map(function(flow){
|
||||
return <div>{flow.request.method} {flow.request.scheme}://{flow.request.host}{flow.request.path}</div>;
|
||||
});
|
||||
return (
|
||||
<main onScroll={this.onScroll}>
|
||||
<table className="flow-table">
|
||||
<FlowTableHead columns={this.state.columns}/>
|
||||
<FlowTableBody onClick={this.onClick} columns={this.state.columns} flows={this.state.flows}/>
|
||||
<FlowTableHead ref="head"
|
||||
columns={this.state.columns}/>
|
||||
<FlowTableBody ref="body"
|
||||
selectFlow={this.selectFlow}
|
||||
onKeyDown={this.onKeyDown}
|
||||
selected={this.state.selected}
|
||||
columns={this.state.columns}
|
||||
flows={this.state.flows}/>
|
||||
</table>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -26,7 +26,7 @@ var ProxyAppMain = React.createClass({
|
||||
return (
|
||||
<div id="container">
|
||||
<Header settings={this.state.settings}/>
|
||||
<div id="main"><this.props.activeRouteHandler/></div>
|
||||
<this.props.activeRouteHandler/>
|
||||
{this.state.settings.showEventLog ? <EventLog/> : null}
|
||||
<Footer settings={this.state.settings}/>
|
||||
</div>
|
||||
|
@ -35,9 +35,8 @@ _.extend(FlowView.prototype, EventEmitter.prototype, {
|
||||
this.emit("change");
|
||||
},
|
||||
_update: function(flow){
|
||||
console.debug("FIXME: Use UUID");
|
||||
var idx = _.findIndex(this.flows, function(f){
|
||||
return flow.request.timestamp_start == f.request.timestamp_start;
|
||||
return flow.id === f.id;
|
||||
});
|
||||
|
||||
if(idx < 0){
|
||||
@ -61,7 +60,13 @@ _.extend(_FlowStore.prototype, EventEmitter.prototype, {
|
||||
var view = new FlowView(this, !since);
|
||||
|
||||
$.getJSON("/static/flows.json", function(flows){
|
||||
flows = flows.concat(_.cloneDeep(flows)).concat(_.cloneDeep(flows));
|
||||
var id = 1;
|
||||
flows.forEach(function(flow){
|
||||
flow.id = "uuid-"+id++;
|
||||
})
|
||||
view.add_bulk(flows);
|
||||
|
||||
});
|
||||
|
||||
return view;
|
||||
|
@ -11,3 +11,13 @@ var AutoScrollMixin = {
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
var Key = {
|
||||
UP: 38,
|
||||
DOWN: 40,
|
||||
LEFT: 37,
|
||||
RIGHT: 39,
|
||||
ENTER: 13,
|
||||
ESC: 27
|
||||
}
|
Loading…
Reference in New Issue
Block a user