[web] rewrite ProxyApp and MainView with es6

This commit is contained in:
Jason 2016-06-09 13:35:41 +08:00
parent 7707d096d2
commit 851bb4bf68
6 changed files with 385 additions and 356 deletions

5
web/.editorconfig Normal file
View File

@ -0,0 +1,5 @@
[*]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true

View File

@ -4,30 +4,38 @@ import {applyMiddleware, createStore} from 'redux'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import createLogger from 'redux-logger' import createLogger from 'redux-logger'
import thunkMiddleware from 'redux-thunk' import thunkMiddleware from 'redux-thunk'
import { Route, Router as ReactRouter, hashHistory, Redirect } from "react-router"
import Connection from "./connection" import Connection from "./connection"
import {App} from "./components/proxyapp.js" import ProxyApp from "./components/ProxyApp"
import rootReducer from './ducks/index'; import MainView from './components/MainView'
import {addLogEntry} from "./ducks/eventLog"; import rootReducer from './ducks/index'
import { addLogEntry } from "./ducks/eventLog"
// logger must be last // logger must be last
const logger = createLogger();
const store = createStore( const store = createStore(
rootReducer, rootReducer,
applyMiddleware(thunkMiddleware, logger) applyMiddleware(thunkMiddleware, createLogger())
); )
window.onerror = function (msg) { window.addEventListener('error', msg => {
store.dispatch(addLogEntry(msg)); store.dispatch(addLogEntry(msg))
}; })
// @todo remove this
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
window.ws = new Connection("/updates", store.dispatch); window.ws = new Connection("/updates", store.dispatch)
render( render(
<Provider store={store}>{App}</Provider>, <Provider store={store}>
<ReactRouter history={hashHistory}>
<Redirect from="/" to="/flows" />
<Route path="/" component={ProxyApp}>
<Route path="flows" component={MainView}/>
<Route path="flows/:flowId/:detailTab" component={MainView}/>
</Route>
</ReactRouter>
</Provider>,
document.getElementById("mitmproxy") document.getElementById("mitmproxy")
); )
})
});

View File

@ -0,0 +1,191 @@
import React, { Component } from "react"
import { FlowActions } from "../actions.js"
import { Query } from "../actions.js"
import { Key } from "../utils.js"
import { Splitter } from "./common.js"
import FlowTable from "./flowtable.js"
import FlowView from "./flowview/index.js"
import { connect } from 'react-redux'
import { selectFlow, setFilter, setHighlight } from "../ducks/flows"
class MainView extends Component {
/**
* @todo move to actions
* @todo replace with mapStateToProps
*/
componentWillReceiveProps(nextProps) {
// Update redux store with route changes
if (nextProps.routeParams.flowId !== (nextProps.selectedFlow || {}).id) {
this.props.selectFlow(nextProps.routeParams.flowId)
}
if (nextProps.location.query[Query.SEARCH] !== nextProps.filter) {
this.props.setFilter(nextProps.location.query[Query.SEARCH], false)
}
if (nextProps.location.query[Query.HIGHLIGHT] !== nextProps.highlight) {
this.props.setHighlight(nextProps.location.query[Query.HIGHLIGHT], false)
}
}
/**
* @todo move to actions
*/
selectFlow(flow) {
if (flow) {
this.props.updateLocation(`/flows/${flow.id}/${this.props.routeParams.detailTab || "request"}`)
} else {
this.props.updateLocation("/flows")
}
}
/**
* @todo move to actions
*/
selectFlowRelative(shift) {
const { flows, routeParams, selectedFlow } = this.props
let index = 0
if (!routeParams.flowId) {
if (shift < 0) {
index = flows.length - 1
}
} else {
index = Math.min(
Math.max(0, flows.indexOf(selectedFlow) + shift),
flows.length - 1
)
}
this.selectFlow(flows[index])
}
/**
* @todo move to actions
*/
onMainKeyDown(e) {
var flow = this.props.selectedFlow
if (e.ctrlKey) {
return
}
switch (e.keyCode) {
case Key.K:
case Key.UP:
this.selectFlowRelative(-1)
break
case Key.J:
case Key.DOWN:
this.selectFlowRelative(+1)
break
case Key.SPACE:
case Key.PAGE_DOWN:
this.selectFlowRelative(+10)
break
case Key.PAGE_UP:
this.selectFlowRelative(-10)
break
case Key.END:
this.selectFlowRelative(+1e10)
break
case Key.HOME:
this.selectFlowRelative(-1e10)
break
case Key.ESC:
this.selectFlow(null)
break
case Key.H:
case Key.LEFT:
if (this.refs.flowDetails) {
this.refs.flowDetails.nextTab(-1)
}
break
case Key.L:
case Key.TAB:
case Key.RIGHT:
if (this.refs.flowDetails) {
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 {
FlowActions.delete(flow)
}
}
break
case Key.A:
if (e.shiftKey) {
FlowActions.accept_all()
} else if (flow && flow.intercepted) {
FlowActions.accept(flow)
}
break
case Key.R:
if (!e.shiftKey && flow) {
FlowActions.replay(flow)
}
break
case Key.V:
if (e.shiftKey && flow && flow.modified) {
FlowActions.revert(flow)
}
break
case Key.E:
if (this.refs.flowDetails) {
this.refs.flowDetails.promptEdit()
}
break
case Key.SHIFT:
break
default:
console.debug("keydown", e.keyCode)
return
}
e.preventDefault()
}
render() {
const { selectedFlow } = this.props
return (
<div className="main-view">
<FlowTable
ref="flowTable"
selectFlow={this.selectFlow}
selected={selectedFlow}
/>
{selectedFlow && [
<Splitter key="splitter"/>,
<FlowView
key="flowDetails"
ref="flowDetails"
tab={this.props.routeParams.detailTab}
query={this.props.query}
updateLocation={this.props.updateLocation}
flow={selectedFlow}
/>
]}
</div>
)
}
}
export default connect(
state => ({
flows: state.flows.view,
filter: state.flows.filter,
highlight: state.flows.highlight,
selectedFlow: state.flows.all.byId[state.flows.selected[0]]
}),
dispatch => ({
selectFlow: flowId => dispatch(selectFlow(flowId)),
setFilter: filter => dispatch(setFilter(filter)),
setHighlight: highlight => dispatch(setHighlight(highlight))
}),
undefined,
{ withRef: true }
)(MainView)

View File

@ -0,0 +1,163 @@
import React, { Component, PropTypes } from "react"
import ReactDOM from "react-dom"
import _ from "lodash"
import { connect } from 'react-redux'
import { Splitter } from "./common.js"
import { Header, MainMenu } from "./header.js"
import EventLog from "./eventlog.js"
import Footer from "./footer.js"
import { SettingsStore } from "../store/store.js"
import { Key } from "../utils.js"
class ProxyAppMain extends Component {
static childContextTypes = {
returnFocus: PropTypes.func.isRequired,
location: PropTypes.object.isRequired,
}
static contextTypes = {
router: PropTypes.object.isRequired,
}
constructor() {
this.settingsStore = new SettingsStore()
// Default Settings before fetch
_.extend(this.settingsStore.dict, {})
this.state = { settings: this.settingsStore.dict }
}
/**
* @todo move to actions
*/
updateLocation(pathname, queryUpdate) {
if (pathname === undefined) {
pathname = this.props.location.pathname
}
const query = this.props.location.query
for (const key of Object.keys(queryUpdate || {})) {
query[i] = queryUpdate[i] || undefined
}
this.context.router.replace({pathname, query})
}
/**
* @todo pass in with props
*/
getQuery() {
// For whatever reason, react-router always returns the same object, which makes comparing
// the current props with nextProps impossible. As a workaround, we just clone the query object.
return _.clone(this.props.location.query)
}
/**
* @todo remove settings store
* @todo connect websocket here
* @todo listen to window's key events
*/
componentDidMount() {
this.focus()
this.settingsStore.addListener("recalculate", this.onSettingsChange)
}
/**
* @todo remove settings store
* @todo disconnect websocket here
* @todo stop listening to window's key events
*/
componentWillUnmount() {
this.settingsStore.removeListener("recalculate", this.onSettingsChange)
}
/**
* @todo move to actions
*/
onSettingsChange() {
this.setState({ settings: this.settingsStore.dict })
}
/**
* @todo use props
*/
getChildContext() {
return {
returnFocus: this.focus,
location: this.props.location
}
}
/**
* @todo remove it
*/
focus() {
document.activeElement.blur()
window.getSelection().removeAllRanges()
ReactDOM.findDOMNode(this).focus()
}
/**
* @todo move to actions
*/
onKeydown(e) {
let name = null
switch (e.keyCode) {
case Key.I:
name = "intercept"
break
case Key.L:
name = "search"
break
case Key.H:
name = "highlight"
break
default:
let main = this.refs.view
if (this.refs.view.getWrappedInstance) {
main = this.refs.view.getWrappedInstance()
}
if (main.onMainKeyDown) {
main.onMainKeyDown(e)
}
return // don't prevent default then
}
if (name) {
const headerComponent = this.refs.header
headerComponent.setState({active: MainMenu}, function () {
headerComponent.refs.active.refs[name].select()
})
}
e.preventDefault()
}
render() {
const { showEventLog, location, children } = this.props
const { settings } = this.state
const query = this.getQuery()
return (
<div id="container" tabIndex="0" onKeyDown={this.onKeydown}>
<Header ref="header" settings={settings} updateLocation={this.updateLocation} query={query} />
{React.cloneElement(
children,
{ ref: "view", location, query, updateLocation: this.updateLocation }
)}
{showEventLog && [
<Splitter key="splitter" axis="y"/>,
<EventLog key="eventlog"/>
]}
<Footer settings={settings}/>
</div>
)
}
})
export default connect(
state => ({
showEventLog: state.eventLog.visible
})
)(ProxyAppMain)

View File

@ -1,184 +0,0 @@
import React from "react";
import {FlowActions} from "../actions.js";
import {Query} from "../actions.js";
import {Key} from "../utils.js";
import {Splitter} from "./common.js"
import FlowTable from "./flowtable.js";
import FlowView from "./flowview/index.js";
import {connect} from 'react-redux'
import {selectFlow, setFilter, setHighlight} from "../ducks/flows";
var MainView = React.createClass({
componentWillReceiveProps: function (nextProps) {
// Update redux store with route changes
if(nextProps.routeParams.flowId !== (nextProps.selectedFlow || {}).id) {
this.props.selectFlow(nextProps.routeParams.flowId)
}
if(nextProps.location.query[Query.SEARCH] !== nextProps.filter) {
this.props.setFilter(nextProps.location.query[Query.SEARCH], false)
}
if (nextProps.location.query[Query.HIGHLIGHT] !== nextProps.highlight) {
this.props.setHighlight(nextProps.location.query[Query.HIGHLIGHT], false)
}
},
selectFlow: function (flow) {
// TODO: This belongs into redux
if (flow) {
let tab = this.props.routeParams.detailTab || "request";
this.props.updateLocation(`/flows/${flow.id}/${tab}`);
} else {
this.props.updateLocation("/flows");
}
},
selectFlowRelative: function (shift) {
// TODO: This belongs into redux
let flows = this.props.flows,
index
if (!this.props.routeParams.flowId) {
if (shift < 0) {
index = flows.length - 1
} else {
index = 0
}
} else {
index = flows.indexOf(this.props.selectedFlow)
index = Math.min(
Math.max(0, index + shift),
flows.length - 1
)
}
this.selectFlow(flows[index])
},
onMainKeyDown: function (e) {
var flow = this.props.selectedFlow;
if (e.ctrlKey) {
return;
}
switch (e.keyCode) {
case Key.K:
case Key.UP:
this.selectFlowRelative(-1);
break;
case Key.J:
case Key.DOWN:
this.selectFlowRelative(+1);
break;
case Key.SPACE:
case Key.PAGE_DOWN:
this.selectFlowRelative(+10);
break;
case Key.PAGE_UP:
this.selectFlowRelative(-10);
break;
case Key.END:
this.selectFlowRelative(+1e10);
break;
case Key.HOME:
this.selectFlowRelative(-1e10);
break;
case Key.ESC:
this.selectFlow(null);
break;
case Key.H:
case Key.LEFT:
if (this.refs.flowDetails) {
this.refs.flowDetails.nextTab(-1);
}
break;
case Key.L:
case Key.TAB:
case Key.RIGHT:
if (this.refs.flowDetails) {
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 {
FlowActions.delete(flow);
}
}
break;
case Key.A:
if (e.shiftKey) {
FlowActions.accept_all();
} else if (flow && flow.intercepted) {
FlowActions.accept(flow);
}
break;
case Key.R:
if (!e.shiftKey && flow) {
FlowActions.replay(flow);
}
break;
case Key.V:
if (e.shiftKey && flow && flow.modified) {
FlowActions.revert(flow);
}
break;
case Key.E:
if (this.refs.flowDetails) {
this.refs.flowDetails.promptEdit();
}
break;
case Key.SHIFT:
break;
default:
console.debug("keydown", e.keyCode);
return;
}
e.preventDefault();
},
render: function () {
var details = null;
if (this.props.selectedFlow) {
details = [
<Splitter key="splitter"/>,
<FlowView
key="flowDetails"
ref="flowDetails"
tab={this.props.routeParams.detailTab}
query={this.props.query}
updateLocation={this.props.updateLocation}
flow={this.props.selectedFlow}/>
]
}
return (
<div className="main-view">
<FlowTable ref="flowTable"
selectFlow={this.selectFlow}
selected={this.props.selectedFlow} />
{details}
</div>
);
}
});
const MainViewContainer = connect(
state => ({
flows: state.flows.view,
filter: state.flows.filter,
highlight: state.flows.highlight,
selectedFlow: state.flows.all.byId[state.flows.selected[0]]
}),
dispatch => ({
selectFlow: flowId => dispatch(selectFlow(flowId)),
setFilter: filter => dispatch(setFilter(filter)),
setHighlight: highlight => dispatch(setHighlight(highlight))
}),
undefined,
{withRef: true}
)(MainView);
export default MainViewContainer;

View File

@ -1,154 +0,0 @@
import React from "react";
import ReactDOM from "react-dom";
import _ from "lodash";
import {connect} from 'react-redux'
import { Route, Router as ReactRouter, hashHistory, Redirect} from "react-router"
import {Splitter} from "./common.js"
import MainView from "./mainview.js";
import Footer from "./footer.js";
import {Header, MainMenu} from "./header.js";
import EventLog from "./eventlog.js"
import {SettingsStore} from "../store/store.js";
import {Key} from "../utils.js";
//TODO: Move out of here, just a stub.
var Reports = React.createClass({
render: function () {
return <div>ReportEditor</div>;
}
});
var ProxyAppMain = React.createClass({
childContextTypes: {
returnFocus: React.PropTypes.func.isRequired,
location: React.PropTypes.object.isRequired,
},
contextTypes: {
router: React.PropTypes.object.isRequired
},
updateLocation: function (pathname, queryUpdate) {
if (pathname === undefined) {
pathname = this.props.location.pathname;
}
var query = this.props.location.query;
if (queryUpdate !== undefined) {
for (var i in queryUpdate) {
if (queryUpdate.hasOwnProperty(i)) {
query[i] = queryUpdate[i] || undefined; //falsey values shall be removed.
}
}
}
this.context.router.replace({pathname, query});
},
getQuery: function () {
// For whatever reason, react-router always returns the same object, which makes comparing
// the current props with nextProps impossible. As a workaround, we just clone the query object.
return _.clone(this.props.location.query);
},
componentDidMount: function () {
this.focus();
this.settingsStore.addListener("recalculate", this.onSettingsChange);
},
componentWillUnmount: function () {
this.settingsStore.removeListener("recalculate", this.onSettingsChange);
},
onSettingsChange: function () {
this.setState({ settings: this.settingsStore.dict });
},
getChildContext: function () {
return {
returnFocus: this.focus,
location: this.props.location
};
},
getInitialState: function () {
var settingsStore = new SettingsStore();
this.settingsStore = settingsStore;
// Default Settings before fetch
_.extend(settingsStore.dict, {});
return {
settings: settingsStore.dict,
};
},
focus: function () {
document.activeElement.blur();
window.getSelection().removeAllRanges();
ReactDOM.findDOMNode(this).focus();
},
getMainComponent: function () {
return this.refs.view.getWrappedInstance ? this.refs.view.getWrappedInstance() : this.refs.view;
},
onKeydown: function (e) {
var selectFilterInput = function (name) {
var headerComponent = this.refs.header;
headerComponent.setState({active: MainMenu}, function () {
headerComponent.refs.active.refs[name].select();
});
}.bind(this);
switch (e.keyCode) {
case Key.I:
selectFilterInput("intercept");
break;
case Key.L:
selectFilterInput("search");
break;
case Key.H:
selectFilterInput("highlight");
break;
default:
var main = this.getMainComponent();
if (main.onMainKeyDown) {
main.onMainKeyDown(e);
}
return; // don't prevent default then
}
e.preventDefault();
},
render: function () {
var query = this.getQuery();
var eventlog;
if (this.props.showEventLog) {
eventlog = [
<Splitter key="splitter" axis="y"/>,
<EventLog key="eventlog"/>
];
} else {
eventlog = null;
}
return (
<div id="container" tabIndex="0" onKeyDown={this.onKeydown}>
<Header ref="header" settings={this.state.settings} updateLocation={this.updateLocation} query={query} />
{React.cloneElement(
this.props.children,
{ ref: "view", location: this.props.location , updateLocation: this.updateLocation, query }
)}
{eventlog}
<Footer settings={this.state.settings}/>
</div>
);
}
});
const AppContainer = connect(
state => ({
showEventLog: state.eventLog.visible
})
)(ProxyAppMain);
export var App = (
<ReactRouter history={hashHistory}>
<Redirect from="/" to="/flows" />
<Route path="/" component={AppContainer}>
<Route path="flows" component={MainView}/>
<Route path="flows/:flowId/:detailTab" component={MainView}/>
<Route path="reports" component={Reports}/>
</Route>
</ReactRouter>
);