web: completely move flow state to redux

This commit is contained in:
Maximilian Hils 2016-06-04 18:53:41 -07:00
parent e880f532ad
commit d53a2de0ba
10 changed files with 434 additions and 703 deletions

View File

@ -0,0 +1,2 @@
from mitmproxy.web import master
__all__ = ["master"]

File diff suppressed because it is too large Load Diff

View File

@ -8,12 +8,14 @@ import shallowEqual from "shallowequal";
import AutoScroll from "./helpers/AutoScroll";
import {calcVScroll} from "./helpers/VirtualScroll";
import flowtable_columns from "./flowtable-columns.js";
import Filt from "../filt/filt";
FlowRow.propTypes = {
selectFlow: React.PropTypes.func.isRequired,
columns: React.PropTypes.array.isRequired,
flow: React.PropTypes.object.isRequired,
highlighted: React.PropTypes.bool,
highlight: React.PropTypes.string,
selected: React.PropTypes.bool,
};
@ -22,7 +24,7 @@ function FlowRow(props) {
const className = classNames({
"selected": props.selected,
"highlighted": props.highlighted,
"highlighted": props.highlight && parseFilter(props.highlight)(flow),
"intercepted": flow.intercepted,
"has-request": flow.request,
"has-response": flow.response,
@ -39,9 +41,12 @@ function FlowRow(props) {
const FlowRowContainer = connect(
(state, ownProps) => ({
flow: state.flows.all.byId[ownProps.flowId]
flow: state.flows.all.byId[ownProps.flowId],
highlight: state.flows.highlight,
selected: state.flows.selected.indexOf(ownProps.flowId) >= 0
}),
dispatch => ({
(dispatch, ownProps) => ({
})
)(FlowRow);
@ -102,10 +107,6 @@ class FlowTableHead extends React.Component {
class FlowTable extends React.Component {
static contextTypes = {
view: React.PropTypes.object.isRequired,
};
static propTypes = {
rowHeight: React.PropTypes.number,
};
@ -117,26 +118,23 @@ class FlowTable extends React.Component {
constructor(props, context) {
super(props, context);
this.state = { flows: [], vScroll: calcVScroll() };
this.state = { vScroll: calcVScroll() };
this.onChange = this.onChange.bind(this);
this.onViewportUpdate = this.onViewportUpdate.bind(this);
}
componentWillMount() {
window.addEventListener("resize", this.onViewportUpdate);
this.context.view.addListener("add", this.onChange);
this.context.view.addListener("update", this.onChange);
this.context.view.addListener("remove", this.onChange);
this.context.view.addListener("recalculate", this.onChange);
}
componentWillUnmount() {
window.removeEventListener("resize", this.onViewportUpdate);
this.context.view.removeListener("add", this.onChange);
this.context.view.removeListener("update", this.onChange);
this.context.view.removeListener("remove", this.onChange);
this.context.view.removeListener("recalculate", this.onChange);
}
componentWillReceiveProps(nextProps) {
if(nextProps.selected && nextProps.selected !== this.props.selected){
window.setTimeout(() => this.scrollIntoView(nextProps.selected), 1)
}
}
componentDidUpdate() {
@ -150,7 +148,7 @@ class FlowTable extends React.Component {
const vScroll = calcVScroll({
viewportTop,
viewportHeight: viewport.offsetHeight,
itemCount: this.state.flows.length,
itemCount: this.props.flows.length,
rowHeight: this.props.rowHeight,
});
@ -160,13 +158,9 @@ class FlowTable extends React.Component {
}
}
onChange() {
this.setState({ flows: this.context.view.list });
}
scrollIntoView(flow) {
const viewport = ReactDOM.findDOMNode(this);
const index = this.context.view.indexOf(flow);
const index = this.props.flows.indexOf(flow);
const rowHeight = this.props.rowHeight;
const head = ReactDOM.findDOMNode(this.refs.head);
@ -188,8 +182,7 @@ class FlowTable extends React.Component {
render() {
const vScroll = this.state.vScroll;
const highlight = this.context.view._highlight;
const flows = this.state.flows.slice(vScroll.start, vScroll.end);
const flows = this.props.flows.slice(vScroll.start, vScroll.end);
const transform = `translate(0,${this.state.viewportTop}px)`;
@ -206,11 +199,9 @@ class FlowTable extends React.Component {
<tr style={{ height: vScroll.paddingTop }}></tr>
{flows.map(flow => (
<FlowRowContainer
flowId={flow.id}
key={flow.id}
flowId={flow.id}
columns={flowtable_columns}
selected={flow === this.props.selected}
highlighted={highlight && highlight[flow.id]}
selectFlow={this.props.selectFlow}
/>
))}
@ -222,4 +213,17 @@ class FlowTable extends React.Component {
}
}
export default AutoScroll(FlowTable);
FlowTable = AutoScroll(FlowTable)
const parseFilter = _.memoize(Filt.parse)
const FlowTableContainer = connect(
state => ({
flows: state.flows.view,
}),
dispatch => ({
})
)(FlowTable)
export default FlowTableContainer;

View File

@ -3,128 +3,59 @@ import React from "react";
import {FlowActions} from "../actions.js";
import {Query} from "../actions.js";
import {Key} from "../utils.js";
import {StoreView} from "../store/view.js";
import Filt from "../filt/filt.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({
contextTypes: {
flowStore: React.PropTypes.object.isRequired,
},
childContextTypes: {
view: React.PropTypes.object.isRequired,
},
getChildContext: function () {
return {
view: this.state.view
};
},
getInitialState: function () {
var sortKeyFun = false;
var view = new StoreView(this.context.flowStore, this.getViewFilt(), sortKeyFun);
view.addListener("recalculate", this.onRecalculate);
view.addListener("add", this.onUpdate);
view.addListener("update", this.onUpdate);
view.addListener("remove", this.onUpdate);
view.addListener("remove", this.onRemove);
return {
view: view,
sortKeyFun: sortKeyFun
};
},
componentWillUnmount: function () {
this.state.view.close();
},
getViewFilt: function () {
try {
var filtStr = this.props.query[Query.SEARCH];
var filt = filtStr ? Filt.parse(filtStr) : () => true;
var highlightStr = this.props.query[Query.HIGHLIGHT];
var highlight = highlightStr ? Filt.parse(highlightStr) : () => false;
} catch (e) {
console.error("Error when processing filter: " + e);
}
var fun = function filter_and_highlight(flow) {
if (!this._highlight) {
this._highlight = {};
}
this._highlight[flow.id] = highlight(flow);
return filt(flow);
};
fun.highlightStr = highlightStr;
fun.filtStr = filtStr;
return fun;
},
componentWillReceiveProps: function (nextProps) {
var filterChanged = this.state.view.filt.filtStr !== nextProps.location.query[Query.SEARCH];
var highlightChanged = this.state.view.filt.highlightStr !== nextProps.location.query[Query.HIGHLIGHT];
if (filterChanged || highlightChanged) {
this.state.view.recalculate(this.getViewFilt(), this.state.sortKeyFun);
// Update redux store with route changes
if(nextProps.routeParams.flowId !== (nextProps.selectedFlow || {}).id) {
this.props.selectFlow(nextProps.routeParams.flowId)
}
},
onRecalculate: function () {
this.forceUpdate();
var selected = this.getSelected();
if (selected) {
this.refs.flowTable.scrollIntoView(selected);
if(nextProps.location.query[Query.SEARCH] !== nextProps.filter) {
this.props.setFilter(nextProps.location.query[Query.SEARCH], false)
}
},
onUpdate: function (flow) {
if (flow.id === this.props.routeParams.flowId) {
this.forceUpdate();
}
},
onRemove: function (flow_id, index) {
if (flow_id === this.props.routeParams.flowId) {
var flow_to_select = this.state.view.list[Math.min(index, this.state.view.list.length - 1)];
this.selectFlow(flow_to_select);
if (nextProps.location.query[Query.HIGHLIGHT] !== nextProps.highlight) {
this.props.setHighlight(nextProps.location.query[Query.HIGHLIGHT], false)
}
},
setSortKeyFun: function (sortKeyFun) {
this.setState({
sortKeyFun: sortKeyFun
});
this.state.view.recalculate(this.getViewFilt(), sortKeyFun);
// FIXME: Move to redux. This requires that sortKeyFun is not a function anymore.
},
selectFlow: function (flow) {
// TODO: This belongs into redux
if (flow) {
var tab = this.props.routeParams.detailTab || "request";
let tab = this.props.routeParams.detailTab || "request";
this.props.updateLocation(`/flows/${flow.id}/${tab}`);
this.refs.flowTable.scrollIntoView(flow);
} else {
this.props.updateLocation("/flows");
}
},
selectFlowRelative: function (shift) {
var flows = this.state.view.list;
var index;
// TODO: This belongs into redux
let flows = this.props.flows,
index
if (!this.props.routeParams.flowId) {
if (shift < 0) {
index = flows.length - 1;
index = flows.length - 1
} else {
index = 0;
index = 0
}
} else {
var currFlowId = this.props.routeParams.flowId;
var i = flows.length;
while (i--) {
if (flows[i].id === currFlowId) {
index = i;
break;
}
}
index = flows.indexOf(this.props.selectedFlow)
index = Math.min(
Math.max(0, index + shift),
flows.length - 1);
flows.length - 1
)
}
this.selectFlow(flows[index]);
this.selectFlow(flows[index])
},
onMainKeyDown: function (e) {
var flow = this.getSelected();
var flow = this.props.selectedFlow;
if (e.ctrlKey) {
return;
}
@ -210,14 +141,10 @@ var MainView = React.createClass({
}
e.preventDefault();
},
getSelected: function () {
return this.context.flowStore.get(this.props.routeParams.flowId);
},
render: function () {
var selected = this.getSelected();
var details;
if (selected) {
var details = null;
if (this.props.selectedFlow) {
details = [
<Splitter key="splitter"/>,
<FlowView
@ -226,10 +153,8 @@ var MainView = React.createClass({
tab={this.props.routeParams.detailTab}
query={this.props.query}
updateLocation={this.props.updateLocation}
flow={selected}/>
];
} else {
details = null;
flow={this.props.selectedFlow}/>
]
}
return (
@ -237,11 +162,27 @@ var MainView = React.createClass({
<FlowTable ref="flowTable"
selectFlow={this.selectFlow}
setSortKeyFun={this.setSortKeyFun}
selected={selected} />
selected={this.props.selectedFlow} />
{details}
</div>
);
}
});
export default MainView;
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

@ -9,7 +9,7 @@ import MainView from "./mainview.js";
import Footer from "./footer.js";
import {Header, MainMenu} from "./header.js";
import EventLog from "./eventlog.js"
import {FlowStore, SettingsStore} from "../store/store.js";
import {SettingsStore} from "../store/store.js";
import {Key} from "../utils.js";
@ -23,7 +23,6 @@ var Reports = React.createClass({
var ProxyAppMain = React.createClass({
childContextTypes: {
flowStore: React.PropTypes.object.isRequired,
returnFocus: React.PropTypes.func.isRequired,
location: React.PropTypes.object.isRequired,
},
@ -61,13 +60,11 @@ var ProxyAppMain = React.createClass({
},
getChildContext: function () {
return {
flowStore: this.state.flowStore,
returnFocus: this.focus,
location: this.props.location
};
},
getInitialState: function () {
var flowStore = new FlowStore();
var settingsStore = new SettingsStore();
this.settingsStore = settingsStore;
@ -75,7 +72,6 @@ var ProxyAppMain = React.createClass({
_.extend(settingsStore.dict, {});
return {
settings: settingsStore.dict,
flowStore: flowStore,
};
},
focus: function () {
@ -84,7 +80,7 @@ var ProxyAppMain = React.createClass({
ReactDOM.findDOMNode(this).focus();
},
getMainComponent: function () {
return this.refs.view;
return this.refs.view.getWrappedInstance ? this.refs.view.getWrappedInstance() : this.refs.view;
},
onKeydown: function (e) {

View File

@ -35,7 +35,7 @@ export default function reducer(state = defaultState, action) {
...state,
filter,
filteredEvents: updateViewFilter(
state.events.list,
state.events,
x => filter[x.level]
)
}

View File

@ -1,6 +1,11 @@
import makeList from "./utils/list"
import Filt from "../filt/filt"
import {updateViewFilter, updateViewList} from "./utils/view"
export const UPDATE_FLOWS = "UPDATE_FLOWS"
export const SET_FILTER = "SET_FLOW_FILTER"
export const SET_HIGHLIGHT = "SET_FLOW_HIGHLIGHT"
export const SELECT_FLOW = "SELECT_FLOW"
const {
reduceList,
@ -11,6 +16,14 @@ const {
const defaultState = {
all: reduceList(),
selected: [],
view: [],
filter: undefined,
highlight: undefined,
}
function makeFilterFn(filter) {
return filter ? Filt.parse(filter) : () => true;
}
export default function reducer(state = defaultState, action) {
@ -20,10 +33,48 @@ export default function reducer(state = defaultState, action) {
return {
...state,
all,
view: updateViewList(state.view, state.all, all, action, makeFilterFn(action.filter))
}
case SET_FILTER:
return {
...state,
filter: action.filter,
view: updateViewFilter(state.all, makeFilterFn(action.filter))
}
case SET_HIGHLIGHT:
return {
...state,
highlight: action.highlight
}
case SELECT_FLOW:
return {
...state,
selected: [action.flowId]
}
default:
return state
}
}
export function setFilter(filter) {
return {
type: SET_FILTER,
filter
}
}
export function setHighlight(highlight) {
return {
type: SET_HIGHLIGHT,
highlight
}
}
export function selectFlow(flowId) {
return {
type: SELECT_FLOW,
flowId
}
}
export {updateList as updateFlows, fetchList as fetchFlows}

View File

@ -15,13 +15,15 @@ const makeCompareFn = sortFn => {
return 0
}
}
if (sortFn.reverse)
return (a, b) => compareFn(b, a)
// need to adjust sortedIndexOf as well
// if (sortFn.reverse)
// return (a, b) => compareFn(b, a)
return compareFn
}
const sortedInsert = (list, sortFn, item) => {
let l = [...list, item]
l.indexOf = x => sortedIndexOf(l, x, sortFn)
let compareFn = makeCompareFn(sortFn)
// only sort if sorting order is not correct yet
@ -35,21 +37,54 @@ const sortedInsert = (list, sortFn, item) => {
const sortedRemove = (list, sortFn, item) => {
let itemId = item.id
return list.filter(x => x.id !== itemId)
let l = list.filter(x => x.id !== itemId)
l.indexOf = x => sortedIndexOf(l, x, sortFn)
return l
}
export function sortedIndexOf(list, value, sortFn) {
if (sortFn === false){
let i = 0
while (i < list.length && list[i].id !== value.id){
i++
}
return i
}
let low = 0,
high = list.length,
val = sortFn(value),
mid;
while (low < high) {
mid = (low + high) >>> 1;
if ((sortFn(list[mid]) < val) ) {
low = mid + 1
} else {
high = mid
}
}
// Two flows may have the same sort value.
// we previously determined the leftmost flow with the same sort value,
// so no we need to scan linearly
while (list[low].id !== value.id && sortFn(list[low + 1]) === val) {
low++
}
return low;
}
// for when the list changes
export function updateViewList(state, currentList, nextList, action, filterFn = defaultFilterFn, sortFn = defaultSortFn) {
export function updateViewList(currentView, currentList, nextList, action, filterFn = defaultFilterFn, sortFn = defaultSortFn) {
switch (action.cmd) {
case REQUEST_LIST:
return state
return currentView
case RECEIVE_LIST:
return updateViewFilter(nextList.list, filterFn, sortFn)
return updateViewFilter(nextList, filterFn, sortFn)
case ADD:
if (filterFn(action.item)) {
return sortedInsert(state, sortFn, action.item)
return sortedInsert(currentView, sortFn, action.item)
}
return state
return currentView
case UPDATE:
// let's determine if it's in the view currently and if it should be in the view.
let currentItemState = currentList.byId[action.item.id],
@ -58,30 +93,34 @@ export function updateViewList(state, currentList, nextList, action, filterFn =
shouldBeInView = filterFn(nextItemState)
if (!isInView && shouldBeInView)
return sortedInsert(state, sortFn, action.item)
return sortedInsert(currentView, sortFn, action.item)
if (isInView && !shouldBeInView)
return sortedRemove(state, sortFn, action.item)
if (isInView && shouldBeInView && sortFn(currentItemState) !== sortFn(nextItemState)) {
let s = [...state]
s.sort(sortFn)
return sortedRemove(currentView, sortFn, action.item)
if (isInView && shouldBeInView && sortFn && sortFn(currentItemState) !== sortFn(nextItemState)) {
let s = [...currentView]
s.sort(makeCompareFn(sortFn))
s.indexOf = x => sortedIndexOf(s, x, sortFn)
return s
}
return state
return currentView
case REMOVE:
let isInView_ = filterFn(currentList.byId[action.item.id])
if (isInView_) {
return sortedRemove(state, sortFn, action.item)
return sortedRemove(currentView, sortFn, action.item)
}
return state
return currentView
default:
console.error("Unknown list action: ", action)
return state
return currentView
}
}
export function updateViewFilter(list, filterFn = defaultFilterFn, sortFn = defaultSortFn) {
let filtered = list.filter(filterFn)
if (sortFn)
let filtered = list.list.filter(filterFn)
if (sortFn){
filtered.sort(makeCompareFn(sortFn))
}
filtered.indexOf = x => sortedIndexOf(filtered, x, sortFn)
return filtered
}

View File

@ -6,55 +6,6 @@ import {ActionTypes, StoreCmds} from "../actions.js";
import {AppDispatcher} from "../dispatcher.js";
function ListStore() {
EventEmitter.call(this);
this.reset();
}
_.extend(ListStore.prototype, EventEmitter.prototype, {
add: function (elem) {
if (elem.id in this._pos_map) {
return;
}
this._pos_map[elem.id] = this.list.length;
this.list.push(elem);
this.emit("add", elem);
},
update: function (elem) {
if (!(elem.id in this._pos_map)) {
return;
}
this.list[this._pos_map[elem.id]] = elem;
this.emit("update", elem);
},
remove: function (elem_id) {
if (!(elem_id in this._pos_map)) {
return;
}
this.list.splice(this._pos_map[elem_id], 1);
this._build_map();
this.emit("remove", elem_id);
},
reset: function (elems) {
this.list = elems || [];
this._build_map();
this.emit("recalculate");
},
_build_map: function () {
this._pos_map = {};
for (var i = 0; i < this.list.length; i++) {
var elem = this.list[i];
this._pos_map[elem.id] = i;
}
},
get: function (elem_id) {
return this.list[this._pos_map[elem_id]];
},
index: function (elem_id) {
return this._pos_map[elem_id];
}
});
function DictStore() {
EventEmitter.call(this);
this.reset();
@ -133,12 +84,6 @@ _.extend(LiveStoreMixin.prototype, {
},
});
function LiveListStore(type) {
ListStore.call(this);
LiveStoreMixin.call(this, type);
}
_.extend(LiveListStore.prototype, ListStore.prototype, LiveStoreMixin.prototype);
function LiveDictStore(type) {
DictStore.call(this);
LiveStoreMixin.call(this, type);
@ -146,10 +91,6 @@ function LiveDictStore(type) {
_.extend(LiveDictStore.prototype, DictStore.prototype, LiveStoreMixin.prototype);
export function FlowStore() {
return new LiveListStore(ActionTypes.FLOW_STORE);
}
export function SettingsStore() {
return new LiveDictStore(ActionTypes.SETTINGS_STORE);
}

View File

@ -1,111 +0,0 @@
import {EventEmitter} from 'events';
import _ from "lodash";
import utils from "../utils.js";
function SortByStoreOrder(elem) {
return this.store.index(elem.id);
}
var default_sort = SortByStoreOrder;
var default_filt = function (elem) {
return true;
};
export function StoreView(store, filt, sortfun) {
EventEmitter.call(this);
this.store = store;
this.add = this.add.bind(this);
this.update = this.update.bind(this);
this.remove = this.remove.bind(this);
this.recalculate = this.recalculate.bind(this);
this.store.addListener("add", this.add);
this.store.addListener("update", this.update);
this.store.addListener("remove", this.remove);
this.store.addListener("recalculate", this.recalculate);
this.recalculate(filt, sortfun);
}
_.extend(StoreView.prototype, EventEmitter.prototype, {
close: function () {
this.store.removeListener("add", this.add);
this.store.removeListener("update", this.update);
this.store.removeListener("remove", this.remove);
this.store.removeListener("recalculate", this.recalculate);
this.removeAllListeners();
},
recalculate: function (filt, sortfun) {
filt = filt || this.filt || default_filt;
sortfun = sortfun || this.sortfun || default_sort;
filt = filt.bind(this);
sortfun = sortfun.bind(this);
this.filt = filt;
this.sortfun = sortfun;
this.list = this.store.list.filter(filt);
this.list.sort(function (a, b) {
var akey = sortfun(a);
var bkey = sortfun(b);
if(akey < bkey){
return -1;
} else if(akey > bkey){
return 1;
} else {
return 0;
}
});
this.emit("recalculate");
},
indexOf: function (elem) {
return this.list.indexOf(elem, _.sortedIndexBy(this.list, elem, this.sortfun));
},
add: function (elem) {
if (this.filt(elem)) {
var idx = _.sortedIndexBy(this.list, elem, this.sortfun);
if (idx === this.list.length) { //happens often, .push is way faster.
this.list.push(elem);
} else {
this.list.splice(idx, 0, elem);
}
this.emit("add", elem, idx);
}
},
update: function (elem) {
var idx;
var i = this.list.length;
// Search from the back, we usually update the latest entries.
while (i--) {
if (this.list[i].id === elem.id) {
idx = i;
break;
}
}
if (idx === -1) { //not contained in list
this.add(elem);
} else if (!this.filt(elem)) {
this.remove(elem.id);
} else {
if (this.sortfun(this.list[idx]) !== this.sortfun(elem)) { //sortpos has changed
this.remove(this.list[idx]);
this.add(elem);
} else {
this.list[idx] = elem;
this.emit("update", elem, idx);
}
}
},
remove: function (elem_id) {
var idx = this.list.length;
while (idx--) {
if (this.list[idx].id === elem_id) {
this.list.splice(idx, 1);
this.emit("remove", elem_id, idx);
break;
}
}
}
});