diff options
author | Maximilian Hils <git@maximilianhils.com> | 2016-06-02 23:45:36 -0700 |
---|---|---|
committer | Maximilian Hils <git@maximilianhils.com> | 2016-06-02 23:45:36 -0700 |
commit | e31aa39fc28645fe27646b1e6c6f7e876280ed69 (patch) | |
tree | 8536a77b278d8182205e8ba757e1e1f00ac67d76 /web/src | |
parent | 65fde7f5547f179c80d5858f1ab69583b63fd099 (diff) | |
parent | 5321f15defcef641bf5b7ba39e5c9057d562c5f8 (diff) | |
download | mitmproxy-e31aa39fc28645fe27646b1e6c6f7e876280ed69.tar.gz mitmproxy-e31aa39fc28645fe27646b1e6c6f7e876280ed69.tar.bz2 mitmproxy-e31aa39fc28645fe27646b1e6c6f7e876280ed69.zip |
Merge branch 'redux-ducks'
Diffstat (limited to 'web/src')
-rw-r--r-- | web/src/css/eventlog.less | 8 | ||||
-rw-r--r-- | web/src/css/header.less | 17 | ||||
-rw-r--r-- | web/src/js/app.js | 33 | ||||
-rw-r--r-- | web/src/js/components/common.js | 17 | ||||
-rw-r--r-- | web/src/js/components/eventlog.js | 206 | ||||
-rw-r--r-- | web/src/js/components/header.js | 43 | ||||
-rw-r--r-- | web/src/js/components/proxyapp.js | 17 | ||||
-rw-r--r-- | web/src/js/connection.js | 18 | ||||
-rw-r--r-- | web/src/js/ducks/README.md | 1 | ||||
-rw-r--r-- | web/src/js/ducks/eventLog.js | 61 | ||||
-rw-r--r-- | web/src/js/ducks/index.js | 10 | ||||
-rw-r--r-- | web/src/js/ducks/list.js | 21 | ||||
-rw-r--r-- | web/src/js/ducks/websocket.js | 30 |
13 files changed, 285 insertions, 197 deletions
diff --git a/web/src/css/eventlog.less b/web/src/css/eventlog.less index 26dea3cc..908312cd 100644 --- a/web/src/css/eventlog.less +++ b/web/src/css/eventlog.less @@ -33,6 +33,14 @@ } } + .btn-toggle { + margin-top: -2px; + margin-left: 3px; + padding: 2px 2px; + font-size: 10px; + line-height: 10px; + border-radius: 2px; + } .label { cursor: pointer; vertical-align: middle; diff --git a/web/src/css/header.less b/web/src/css/header.less index b1bd9c04..4813b933 100644 --- a/web/src/css/header.less +++ b/web/src/css/header.less @@ -32,20 +32,3 @@ header { overflow-y: auto; } } - -.menu .toggle-btn { - .make-xs-column(4, @menu-row-gutter-width); - .make-sm-column(3, @menu-row-gutter-width); - .make-lg-column(2, @menu-row-gutter-width); - margin-bottom:5px; -} - -.menu .toggle-btn .btn { - width: 100%; -} - -.menu .toggle-input-btn { - .make-sm-column(6, @menu-row-gutter-width); - .make-lg-column(4, @menu-row-gutter-width); - margin-bottom:5px; -}
\ No newline at end of file diff --git a/web/src/js/app.js b/web/src/js/app.js index e21fa499..fc99f1d2 100644 --- a/web/src/js/app.js +++ b/web/src/js/app.js @@ -1,17 +1,28 @@ import React from "react" -import { render } from 'react-dom' -import $ from "jquery" +import {render} from 'react-dom' +import {applyMiddleware, createStore} from 'redux' +import {Provider} from 'react-redux' +import createLogger from 'redux-logger'; + import Connection from "./connection" -import {app} from "./components/proxyapp.js" -import { EventLogActions } from "./actions.js" +import {App} from "./components/proxyapp.js" +import rootReducer from './ducks/index'; +import {addLogEntry} from "./ducks/eventLog"; -$(function () { - window.ws = new Connection("/updates"); +// logger must be last +const logger = createLogger(); +const store = createStore(rootReducer, applyMiddleware(logger)); - window.onerror = function (msg) { - EventLogActions.add_event(msg); - }; +window.onerror = function (msg) { + store.dispatch(addLogEntry(msg)); +}; - render(app, document.getElementById("mitmproxy")); -}); +document.addEventListener('DOMContentLoaded', () => { + window.ws = new Connection("/updates", store.dispatch); + render( + <Provider store={store}>{App}</Provider>, + document.getElementById("mitmproxy") + ); + +}); diff --git a/web/src/js/components/common.js b/web/src/js/components/common.js index 87c34ffc..3496f1de 100644 --- a/web/src/js/components/common.js +++ b/web/src/js/components/common.js @@ -108,18 +108,17 @@ export var Splitter = React.createClass({ } }); -export const ToggleButton = (props) => - <div className="input-group toggle-btn"> - <div - className={"btn " + (props.checked ? "btn-primary" : "btn-default")} - onClick={props.onToggleChanged}> - <span className={"fa " + (props.checked ? "fa-check-square-o" : "fa-square-o")}> {props.name}</span> - </div> +export const ToggleButton = ({checked, onToggle, text}) => + <div className={"btn btn-toggle " + (checked ? "btn-primary" : "btn-default")} onClick={onToggle}> + <i className={"fa fa-fw " + (checked ? "fa-check-square-o" : "fa-square-o")}/> + + {text} </div>; ToggleButton.propTypes = { - name: React.PropTypes.string.isRequired, - onToggleChanged: React.PropTypes.func.isRequired + checked: React.PropTypes.bool.isRequired, + onToggle: React.PropTypes.func.isRequired, + text: React.PropTypes.string.isRequired }; export class ToggleInputButton extends React.Component { diff --git a/web/src/js/components/eventlog.js b/web/src/js/components/eventlog.js index 6e4f9096..95889a66 100644 --- a/web/src/js/components/eventlog.js +++ b/web/src/js/components/eventlog.js @@ -1,86 +1,72 @@ import React from "react" import ReactDOM from "react-dom" +import {connect} from 'react-redux' import shallowEqual from "shallowequal" -import {Query} from "../actions.js" +import {toggleEventLogFilter, toggleEventLogVisibility} from "../ducks/eventLog" import AutoScroll from "./helpers/AutoScroll"; import {calcVScroll} from "./helpers/VirtualScroll" -import {StoreView} from "../store/view.js" -import _ from "lodash" +import {ToggleButton} from "./common"; -class EventLogContents extends React.Component { +function LogIcon({event}) { + let icon = {web: "html5", debug: "bug"}[event.level] || "info"; + return <i className={`fa fa-fw fa-${icon}`}></i> +} - static contextTypes = { - eventStore: React.PropTypes.object.isRequired, - }; +function LogEntry({event, registerHeight}) { + return <div ref={registerHeight}> + <LogIcon event={event}/> + {event.message} + </div>; +} + +class EventLogContents extends React.Component { static defaultProps = { rowHeight: 18, }; - constructor(props, context) { - super(props, context); - - this.view = new StoreView( - this.context.eventStore, - entry => this.props.filter[entry.level] - ); + constructor(props) { + super(props); this.heights = {}; - this.state = { entries: this.view.list, vScroll: calcVScroll() }; + this.state = {vScroll: calcVScroll()}; - this.onChange = this.onChange.bind(this); this.onViewportUpdate = this.onViewportUpdate.bind(this); } componentDidMount() { window.addEventListener("resize", this.onViewportUpdate); - this.view.addListener("add", this.onChange); - this.view.addListener("recalculate", this.onChange); this.onViewportUpdate(); } componentWillUnmount() { window.removeEventListener("resize", this.onViewportUpdate); - this.view.removeListener("add", this.onChange); - this.view.removeListener("recalculate", this.onChange); - this.view.close(); } componentDidUpdate() { this.onViewportUpdate(); } - componentWillReceiveProps(nextProps) { - if (nextProps.filter !== this.props.filter) { - this.view.recalculate( - entry => nextProps.filter[entry.level] - ); - } - } - onViewportUpdate() { const viewport = ReactDOM.findDOMNode(this); const vScroll = calcVScroll({ - itemCount: this.state.entries.length, + itemCount: this.props.events.length, rowHeight: this.props.rowHeight, viewportTop: viewport.scrollTop, viewportHeight: viewport.offsetHeight, - itemHeights: this.state.entries.map(entry => this.heights[entry.id]), + itemHeights: this.props.events.map(entry => this.heights[entry.id]), }); if (!shallowEqual(this.state.vScroll, vScroll)) { - this.setState({ vScroll }); + this.setState({vScroll}); } } - onChange() { - this.setState({ entries: this.view.list }); - } - - setHeight(id, ref) { - if (ref && !this.heights[id]) { - const height = ReactDOM.findDOMNode(ref).offsetHeight; + setHeight(id, node) { + console.log("setHeight", id, node); + if (node && !this.heights[id]) { + const height = node.offsetHeight; if (this.heights[id] !== height) { this.heights[id] = height; this.onViewportUpdate(); @@ -88,97 +74,81 @@ class EventLogContents extends React.Component { } } - getIcon(level) { - return { web: "html5", debug: "bug" }[level] || "info"; - } - render() { const vScroll = this.state.vScroll; - const entries = this.state.entries.slice(vScroll.start, vScroll.end); + const events = this.props.events + .slice(vScroll.start, vScroll.end) + .map(event => + <LogEntry + event={event} + key={event.id} + registerHeight={(node) => this.setHeight(event.id, node)} + /> + ); return ( <pre onScroll={this.onViewportUpdate}> <div style={{ height: vScroll.paddingTop }}></div> - {entries.map((entry, index) => ( - <div key={entry.id} ref={this.setHeight.bind(this, entry.id)}> - <i className={`fa fa-fw fa-${this.getIcon(entry.level)}`}></i> - {entry.message} - </div> - ))} + {events} <div style={{ height: vScroll.paddingBottom }}></div> </pre> ); } } -ToggleFilter.propTypes = { - name: React.PropTypes.string.isRequired, - toggleLevel: React.PropTypes.func.isRequired, - active: React.PropTypes.bool, -}; - -function ToggleFilter ({ name, active, toggleLevel }) { - let className = "label "; - if (active) { - className += "label-primary"; - } else { - className += "label-default"; - } - - function onClick(event) { - event.preventDefault(); - toggleLevel(name); - } - - return ( - <a - href="#" - className={className} - onClick={onClick}> - {name} - </a> - ); -} +EventLogContents = AutoScroll(EventLogContents); + + +const EventLogContentsContainer = connect( + state => ({ + events: state.eventLog.filteredEvents + }) +)(EventLogContents); + + +export const ToggleEventLog = connect( + state => ({ + checked: state.eventLog.visible + }), + dispatch => ({ + onToggle: () => dispatch(toggleEventLogVisibility()) + }) +)(ToggleButton); + + +const ToggleFilter = connect( + (state, ownProps) => ({ + checked: state.eventLog.filter[ownProps.text] + }), + (dispatch, ownProps) => ({ + onToggle: () => dispatch(toggleEventLogFilter(ownProps.text)) + }) +)(ToggleButton); + + +const EventLog = ({close}) => + <div className="eventlog"> + <div> + Eventlog + <div className="pull-right"> + <ToggleFilter text="debug"/> + <ToggleFilter text="info"/> + <ToggleFilter text="web"/> + <i onClick={close} className="fa fa-close"></i> + </div> + </div> + <EventLogContentsContainer/> + </div>; -const AutoScrollEventLog = AutoScroll(EventLogContents); +EventLog.propTypes = { + close: React.PropTypes.func.isRequired +}; -var EventLog = React.createClass({ - getInitialState() { - return { - filter: { - "debug": false, - "info": true, - "web": true - } - }; - }, - close() { - var d = {}; - d[Query.SHOW_EVENTLOG] = undefined; - this.props.updateLocation(undefined, d); - }, - toggleLevel(level) { - var filter = _.extend({}, this.state.filter); - filter[level] = !filter[level]; - this.setState({filter: filter}); - }, - render() { - return ( - <div className="eventlog"> - <div> - Eventlog - <div className="pull-right"> - <ToggleFilter name="debug" active={this.state.filter.debug} toggleLevel={this.toggleLevel}/> - <ToggleFilter name="info" active={this.state.filter.info} toggleLevel={this.toggleLevel}/> - <ToggleFilter name="web" active={this.state.filter.web} toggleLevel={this.toggleLevel}/> - <i onClick={this.close} className="fa fa-close"></i> - </div> - - </div> - <AutoScrollEventLog filter={this.state.filter}/> - </div> - ); - } -}); +const EventLogContainer = connect( + undefined, + dispatch => ({ + close: () => dispatch(toggleEventLogVisibility()) + }) +)(EventLog); -export default EventLog; +export default EventLogContainer; diff --git a/web/src/js/components/header.js b/web/src/js/components/header.js index adc8bb9b..4152e95c 100644 --- a/web/src/js/components/header.js +++ b/web/src/js/components/header.js @@ -1,6 +1,7 @@ import React from "react"; import ReactDOM from 'react-dom'; import $ from "jquery"; +import {connect} from 'react-redux' import Filt from "../filt/filt.js"; import {Key} from "../utils.js"; @@ -8,6 +9,7 @@ import {ToggleInputButton, ToggleButton} from "./common.js"; import {SettingsActions, FlowActions} from "../actions.js"; import {Query} from "../actions.js"; import {SettingsState} from "./common.js"; +import {ToggleEventLog} from "./eventlog" var FilterDocs = React.createClass({ statics: { @@ -224,26 +226,11 @@ var ViewMenu = React.createClass({ title: "View", route: "flows" }, - toggleEventLog: function () { - var d = {}; - if (this.props.query[Query.SHOW_EVENTLOG]) { - d[Query.SHOW_EVENTLOG] = undefined; - } else { - d[Query.SHOW_EVENTLOG] = "t"; // any non-false value will do it, keep it short - } - - this.props.updateLocation(undefined, d); - console.log('toggleevent'); - }, render: function () { - var showEventLog = this.props.query[Query.SHOW_EVENTLOG]; return ( <div> <div className="menu-row"> - <ToggleButton - checked={showEventLog} - name = "Show Eventlog" - onToggleChanged={this.toggleEventLog}/> + <ToggleEventLog text="Show Event Log"/> </div> <div className="clearfix"></div> </div> @@ -256,29 +243,29 @@ export const OptionMenu = (props) => { return ( <div> <div className="menu-row"> - <ToggleButton name="showhost" + <ToggleButton text="showhost" checked={showhost} - onToggleChanged={() => SettingsActions.update({showhost: !showhost})} + onToggle={() => SettingsActions.update({showhost: !showhost})} /> - <ToggleButton name="no_upstream_cert" + <ToggleButton text="no_upstream_cert" checked={no_upstream_cert} - onToggleChanged={() => SettingsActions.update({no_upstream_cert: !no_upstream_cert})} + onToggle={() => SettingsActions.update({no_upstream_cert: !no_upstream_cert})} /> - <ToggleButton name="rawtcp" + <ToggleButton text="rawtcp" checked={rawtcp} - onToggleChanged={() => SettingsActions.update({rawtcp: !rawtcp})} + onToggle={() => SettingsActions.update({rawtcp: !rawtcp})} /> - <ToggleButton name="http2" + <ToggleButton text="http2" checked={http2} - onToggleChanged={() => SettingsActions.update({http2: !http2})} + onToggle={() => SettingsActions.update({http2: !http2})} /> - <ToggleButton name="anticache" + <ToggleButton text="anticache" checked={anticache} - onToggleChanged={() => SettingsActions.update({anticache: !anticache})} + onToggle={() => SettingsActions.update({anticache: !anticache})} /> - <ToggleButton name="anticomp" + <ToggleButton text="anticomp" checked={anticomp} - onToggleChanged={() => SettingsActions.update({anticomp: !anticomp})} + onToggle={() => SettingsActions.update({anticomp: !anticomp})} /> <ToggleInputButton name="stickyauth" placeholder="Sticky auth filter" checked={Boolean(stickyauth)} diff --git a/web/src/js/components/proxyapp.js b/web/src/js/components/proxyapp.js index f47c5bb4..99b64580 100644 --- a/web/src/js/components/proxyapp.js +++ b/web/src/js/components/proxyapp.js @@ -1,6 +1,8 @@ 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"; @@ -8,7 +10,6 @@ import Footer from "./footer.js"; import {Header, MainMenu} from "./header.js"; import EventLog from "./eventlog.js" import {EventLogStore, FlowStore, SettingsStore} from "../store/store.js"; -import {Query} from "../actions.js"; import {Key} from "../utils.js"; @@ -120,10 +121,10 @@ var ProxyAppMain = React.createClass({ render: function () { var query = this.getQuery(); var eventlog; - if (this.props.location.query[Query.SHOW_EVENTLOG]) { + if (this.props.showEventLog) { eventlog = [ <Splitter key="splitter" axis="y"/>, - <EventLog key="eventlog" updateLocation={this.updateLocation}/> + <EventLog key="eventlog"/> ]; } else { eventlog = null; @@ -142,13 +143,17 @@ var ProxyAppMain = React.createClass({ } }); +const AppContainer = connect( + state => ({ + showEventLog: state.eventLog.visible + }) +)(ProxyAppMain); -import { Route, Router as ReactRouter, hashHistory, Redirect} from "react-router"; -export var app = ( +export var App = ( <ReactRouter history={hashHistory}> <Redirect from="/" to="/flows" /> - <Route path="/" component={ProxyAppMain}> + <Route path="/" component={AppContainer}> <Route path="flows" component={MainView}/> <Route path="flows/:flowId/:detailTab" component={MainView}/> <Route path="reports" component={Reports}/> diff --git a/web/src/js/connection.js b/web/src/js/connection.js index 6177938e..75c2cf25 100644 --- a/web/src/js/connection.js +++ b/web/src/js/connection.js @@ -1,19 +1,22 @@ - import {ConnectionActions, EventLogActions} from "./actions.js"; import {AppDispatcher} from "./dispatcher.js"; +import * as websocketActions from "./ducks/websocket" -function Connection(url) { +export default function Connection(url, dispatch) { if (url[0] === "/") { url = location.origin.replace("http", "ws") + url; } var ws = new WebSocket(url); ws.onopen = function () { + dispatch(websocketActions.connected()); ConnectionActions.open(); + //TODO: fetch stuff! }; - ws.onmessage = function (message) { - var m = JSON.parse(message.data); - AppDispatcher.dispatchServerAction(m); + ws.onmessage = function (m) { + var message = JSON.parse(m.data); + AppDispatcher.dispatchServerAction(message); + dispatch(message); }; ws.onerror = function () { ConnectionActions.error(); @@ -22,8 +25,7 @@ function Connection(url) { ws.onclose = function () { ConnectionActions.close(); EventLogActions.add_event("WebSocket connection closed."); + dispatch(websocketActions.disconnected()); }; return ws; -} - -export default Connection;
\ No newline at end of file +}
\ No newline at end of file diff --git a/web/src/js/ducks/README.md b/web/src/js/ducks/README.md new file mode 100644 index 00000000..9d005f35 --- /dev/null +++ b/web/src/js/ducks/README.md @@ -0,0 +1 @@ +https://github.com/erikras/ducks-modular-redux
\ No newline at end of file diff --git a/web/src/js/ducks/eventLog.js b/web/src/js/ducks/eventLog.js new file mode 100644 index 00000000..2040711c --- /dev/null +++ b/web/src/js/ducks/eventLog.js @@ -0,0 +1,61 @@ +import getList, {ADD} from "./list" +const TOGGLE_FILTER = 'TOGGLE_EVENTLOG_FILTER' +const TOGGLE_VISIBILITY = 'TOGGLE_EVENTLOG_VISIBILITY' +const UPDATE_LIST = "UPDATE_EVENTLOG" + + +const defaultState = { + visible: false, + filter: { + "debug": false, + "info": true, + "web": true + }, + events: getList(), + filteredEvents: [], +} + +export default function reducer(state = defaultState, action) { + switch (action.type) { + case TOGGLE_FILTER: + const filter = { + ...state.filter, + [action.filter]: !state.filter[action.filter] + } + return { + ...state, + filter, + filteredEvents: state.events.list.filter(x => filter[x.level]) + } + case TOGGLE_VISIBILITY: + return { + ...state, + visible: !state.visible + } + case UPDATE_LIST: + const events = getList(state.events, action) + return { + ...state, + events, + filteredEvents: events.list.filter(x => state.filter[x.level]) + } + default: + return state + } +} + + +export function toggleEventLogFilter(filter) { + return {type: TOGGLE_FILTER, filter} +} +export function toggleEventLogVisibility() { + return {type: TOGGLE_VISIBILITY} +} +let id = 0; +export function addLogEntry(message, level = "web") { + return { + type: UPDATE_LIST, + cmd: ADD, + data: {message, level, id: `log-${id++}`} + } +}
\ No newline at end of file diff --git a/web/src/js/ducks/index.js b/web/src/js/ducks/index.js new file mode 100644 index 00000000..3043344c --- /dev/null +++ b/web/src/js/ducks/index.js @@ -0,0 +1,10 @@ +import {combineReducers} from 'redux' +import eventLog from './eventLog.js' +import websocket from './websocket.js' + +const rootReducer = combineReducers({ + eventLog, + websocket, +}) + +export default rootReducer
\ No newline at end of file diff --git a/web/src/js/ducks/list.js b/web/src/js/ducks/list.js new file mode 100644 index 00000000..0b3771e2 --- /dev/null +++ b/web/src/js/ducks/list.js @@ -0,0 +1,21 @@ +export const ADD = 'add' + +const defaultState = { + list: [], + //isFetching: false, + //updateBeforeFetch: [], + indexOf: {}, + //views: {} +}; + +export default function getList(state = defaultState, action = {}) { + switch (action.cmd) { + case ADD: + return { + list: [...state.list, action.data], + indexOf: {...state.indexOf, [action.data.id]: state.list.length}, + } + default: + return state + } +}
\ No newline at end of file diff --git a/web/src/js/ducks/websocket.js b/web/src/js/ducks/websocket.js new file mode 100644 index 00000000..3999dbcf --- /dev/null +++ b/web/src/js/ducks/websocket.js @@ -0,0 +1,30 @@ +const CONNECTED = 'WEBSOCKET_CONNECTED' +const DISCONNECTED = 'WEBSOCKET_DISCONNECTED' + + +const defaultState = { + connected: true, + /* we may want to have an error message attribute here at some point */ +} +export default function reducer(state = defaultState, action) { + switch (action.type) { + case CONNECTED: + return { + connected: true + } + case DISCONNECTED: + return { + connected: false + } + default: + return state + } +} + + +export function connected() { + return {type: CONNECTED} +} +export function disconnected() { + return {type: DISCONNECTED} +}
\ No newline at end of file |