From d53a2de0ba69bea6c7aefa87782ad249cfb4ea76 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sat, 4 Jun 2016 18:53:41 -0700 Subject: web: completely move flow state to redux --- web/src/js/components/flowtable.js | 64 ++++++++-------- web/src/js/components/mainview.js | 149 +++++++++++-------------------------- web/src/js/components/proxyapp.js | 8 +- web/src/js/ducks/eventLog.js | 2 +- web/src/js/ducks/flows.js | 51 +++++++++++++ web/src/js/ducks/utils/view.js | 77 ++++++++++++++----- web/src/js/store/store.js | 59 --------------- web/src/js/store/view.js | 111 --------------------------- 8 files changed, 191 insertions(+), 330 deletions(-) (limited to 'web/src/js') diff --git a/web/src/js/components/flowtable.js b/web/src/js/components/flowtable.js index 1a616eee..0241cd78 100644 --- a/web/src/js/components/flowtable.js +++ b/web/src/js/components/flowtable.js @@ -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 { {flows.map(flow => ( ))} @@ -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; diff --git a/web/src/js/components/mainview.js b/web/src/js/components/mainview.js index 964e82db..22895991 100644 --- a/web/src/js/components/mainview.js +++ b/web/src/js/components/mainview.js @@ -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; - }, +var MainView = React.createClass({ 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 = [ , - ]; - } else { - details = null; + flow={this.props.selectedFlow}/> + ] } return ( @@ -237,11 +162,27 @@ var MainView = React.createClass({ + selected={this.props.selectedFlow} /> {details} ); } }); -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; diff --git a/web/src/js/components/proxyapp.js b/web/src/js/components/proxyapp.js index 9e4bd0a4..e4489e18 100644 --- a/web/src/js/components/proxyapp.js +++ b/web/src/js/components/proxyapp.js @@ -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) { diff --git a/web/src/js/ducks/eventLog.js b/web/src/js/ducks/eventLog.js index e3661fe7..44b67b2c 100644 --- a/web/src/js/ducks/eventLog.js +++ b/web/src/js/ducks/eventLog.js @@ -35,7 +35,7 @@ export default function reducer(state = defaultState, action) { ...state, filter, filteredEvents: updateViewFilter( - state.events.list, + state.events, x => filter[x.level] ) } diff --git a/web/src/js/ducks/flows.js b/web/src/js/ducks/flows.js index fb934489..fdbc42ee 100644 --- a/web/src/js/ducks/flows.js +++ b/web/src/js/ducks/flows.js @@ -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} \ No newline at end of file diff --git a/web/src/js/ducks/utils/view.js b/web/src/js/ducks/utils/view.js index 55fdf6c7..5535ed83 100644 --- a/web/src/js/ducks/utils/view.js +++ b/web/src/js/ducks/utils/view.js @@ -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 } \ No newline at end of file diff --git a/web/src/js/store/store.js b/web/src/js/store/store.js index 65355684..f3e2074f 100644 --- a/web/src/js/store/store.js +++ b/web/src/js/store/store.js @@ -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); } \ No newline at end of file diff --git a/web/src/js/store/view.js b/web/src/js/store/view.js index d8aeba60..e69de29b 100644 --- a/web/src/js/store/view.js +++ b/web/src/js/store/view.js @@ -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; - } - } - } -}); -- cgit v1.2.3