diff options
author | Maximilian Hils <git@maximilianhils.com> | 2016-11-09 15:19:14 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-11-09 15:19:14 +0100 |
commit | f0783a087498a860905f846acca94ac418a5d6be (patch) | |
tree | 12f7bb66bfa87ad53af3d5821ea38ae0e27a0ea9 /web/src/js/ducks | |
parent | 564e56c26275698d3eb59bb3f97240bb0ba499e5 (diff) | |
parent | 77f05178ad23a8bb1f2cc43e1cdcf0593acd43d2 (diff) | |
download | mitmproxy-f0783a087498a860905f846acca94ac418a5d6be.tar.gz mitmproxy-f0783a087498a860905f846acca94ac418a5d6be.tar.bz2 mitmproxy-f0783a087498a860905f846acca94ac418a5d6be.zip |
Merge pull request #1725 from mhils/mitmweb
Mitmweb Improvements
Diffstat (limited to 'web/src/js/ducks')
-rw-r--r-- | web/src/js/ducks/app.js | 27 | ||||
-rw-r--r-- | web/src/js/ducks/eventLog.js | 95 | ||||
-rw-r--r-- | web/src/js/ducks/flowView.js | 195 | ||||
-rw-r--r-- | web/src/js/ducks/flows.js | 248 | ||||
-rw-r--r-- | web/src/js/ducks/index.js | 6 | ||||
-rw-r--r-- | web/src/js/ducks/msgQueue.js | 113 | ||||
-rw-r--r-- | web/src/js/ducks/settings.js | 52 | ||||
-rw-r--r-- | web/src/js/ducks/ui/flow.js | 2 | ||||
-rw-r--r-- | web/src/js/ducks/ui/index.js | 1 | ||||
-rw-r--r-- | web/src/js/ducks/ui/keyboard.js | 13 | ||||
-rw-r--r-- | web/src/js/ducks/utils/list.js | 105 | ||||
-rw-r--r-- | web/src/js/ducks/utils/store.js | 210 | ||||
-rwxr-xr-x | web/src/js/ducks/utils/view.js | 189 | ||||
-rw-r--r-- | web/src/js/ducks/websocket.js | 93 |
14 files changed, 356 insertions, 993 deletions
diff --git a/web/src/js/ducks/app.js b/web/src/js/ducks/app.js deleted file mode 100644 index f1dcb490..00000000 --- a/web/src/js/ducks/app.js +++ /dev/null @@ -1,27 +0,0 @@ -import { connect as wsConnect, disconnect as wsDisconnect } from './websocket' - -export const INIT = 'APP_INIT' - -const defaultState = {} - -export function reduce(state = defaultState, action) { - switch (action.type) { - - default: - return state - } -} - -export function init() { - return dispatch => { - dispatch(wsConnect()) - dispatch({ type: INIT }) - } -} - -export function destruct() { - return dispatch => { - dispatch(wsDisconnect()) - dispatch({ type: DESTRUCT }) - } -} diff --git a/web/src/js/ducks/eventLog.js b/web/src/js/ducks/eventLog.js index f72d7bd6..776e4b08 100644 --- a/web/src/js/ducks/eventLog.js +++ b/web/src/js/ducks/eventLog.js @@ -1,24 +1,15 @@ -import reduceList, * as listActions from './utils/list' -import reduceView, * as viewActions from './utils/view' -import * as websocketActions from './websocket' -import * as msgQueueActions from './msgQueue' +import reduceStore from "./utils/store" +import * as storeActions from "./utils/store" -export const MSG_TYPE = 'UPDATE_EVENTLOG' -export const DATA_URL = '/events' - -export const ADD = 'EVENTLOG_ADD' -export const RECEIVE = 'EVENTLOG_RECEIVE' -export const TOGGLE_VISIBILITY = 'EVENTLOG_TOGGLE_VISIBILITY' -export const TOGGLE_FILTER = 'EVENTLOG_TOGGLE_FILTER' -export const UNKNOWN_CMD = 'EVENTLOG_UNKNOWN_CMD' -export const FETCH_ERROR = 'EVENTLOG_FETCH_ERROR' +export const ADD = 'EVENTS_ADD' +export const RECEIVE = 'EVENTS_RECEIVE' +export const TOGGLE_VISIBILITY = 'EVENTS_TOGGLE_VISIBILITY' +export const TOGGLE_FILTER = 'EVENTS_TOGGLE_FILTER' const defaultState = { - logId: 0, visible: false, filters: { debug: false, info: true, web: true }, - list: reduceList(undefined, {}), - view: reduceView(undefined, {}), + ...reduceStore(undefined, {}), } export default function reduce(state = defaultState, action) { @@ -35,27 +26,14 @@ export default function reduce(state = defaultState, action) { return { ...state, filters, - view: reduceView(state.view, viewActions.updateFilter(state.list.data, log => filters[log.level])), + ...reduceStore(state, storeActions.setFilter(log => filters[log.level])) } case ADD: - const item = { - id: state.logId, - message: action.message, - level: action.level, - } - return { - ...state, - logId: state.logId + 1, - list: reduceList(state.list, listActions.add(item)), - view: reduceView(state.view, viewActions.add(item, log => state.filters[log.level])), - } - case RECEIVE: return { ...state, - list: reduceList(state.list, listActions.receive(action.list)), - view: reduceView(state.view, viewActions.receive(action.list, log => state.filters[log.level])), + ...reduceStore(state, storeActions[action.cmd](action.data, log => state.filters[log.level])) } default: @@ -63,58 +41,25 @@ export default function reduce(state = defaultState, action) { } } -/** - * @public - */ export function toggleFilter(filter) { return { type: TOGGLE_FILTER, filter } } -/** - * @public - * - * @todo move to ui? - */ export function toggleVisibility() { return { type: TOGGLE_VISIBILITY } } -/** - * @public - */ +let logId = 1 // client-side log ids are odd export function add(message, level = 'web') { - return { type: ADD, message, level } -} - -/** - * This action creater takes all WebSocket events - * - * @public websocket - */ -export function handleWsMsg(msg) { - switch (msg.cmd) { - - case websocketActions.CMD_ADD: - return add(msg.data.message, msg.data.level) - - case websocketActions.CMD_RESET: - return fetchData() - - default: - return { type: UNKNOWN_CMD, msg } + let data = { + id: logId, + message, + level, + } + logId += 2 + return { + type: ADD, + cmd: "add", + data } -} - -/** - * @public websocket - */ -export function fetchData() { - return msgQueueActions.fetchData(MSG_TYPE) -} - -/** - * @public msgQueue - */ -export function receiveData(list) { - return { type: RECEIVE, list } } diff --git a/web/src/js/ducks/flowView.js b/web/src/js/ducks/flowView.js deleted file mode 100644 index dd5bea41..00000000 --- a/web/src/js/ducks/flowView.js +++ /dev/null @@ -1,195 +0,0 @@ -import reduceView, * as viewActions from './utils/view' -import * as flowActions from './flows' -import Filt from '../filt/filt' -import { RequestUtils } from '../flow/utils' - -export const UPDATE_FILTER = 'FLOWVIEW_UPDATE_FILTER' -export const UPDATE_SORT = 'FLOWVIEW_UPDATE_SORT' -export const UPDATE_HIGHLIGHT = 'FLOWVIEW_UPDATE_HIGHLIGHT' - - -const sortKeyFuns = { - - TLSColumn: flow => flow.request.scheme, - - PathColumn: flow => RequestUtils.pretty_url(flow.request), - - MethodColumn: flow => flow.request.method, - - StatusColumn: flow => flow.response && flow.response.status_code, - - TimeColumn: flow => flow.response && flow.response.timestamp_end - flow.request.timestamp_start, - - SizeColumn: flow => { - let total = flow.request.contentLength - if (flow.response) { - total += flow.response.contentLength || 0 - } - return total - }, -} - -export function makeFilter(filter) { - if (!filter) { - return - } - return Filt.parse(filter) -} - -export function makeSort({ column, desc }) { - const sortKeyFun = sortKeyFuns[column] - if (!sortKeyFun) { - return - } - return (a, b) => { - const ka = sortKeyFun(a) - const kb = sortKeyFun(b) - if (ka > kb) { - return desc ? -1 : 1 - } - if (ka < kb) { - return desc ? 1 : -1 - } - return 0 - } -} - - -const defaultState = { - highlight: null, - filter: null, - sort: { column: null, desc: false }, - ...reduceView(undefined, {}) -} - -export default function reduce(state = defaultState, action) { - switch (action.type) { - - case UPDATE_HIGHLIGHT: - return { - ...state, - highlight: action.highlight, - } - - case UPDATE_FILTER: - return { - ...reduceView( - state, - viewActions.updateFilter( - action.flows, - makeFilter(action.filter), - makeSort(state.sort) - ) - ), - filter: action.filter, - } - - case UPDATE_SORT: - const sort = { column: action.column, desc: action.desc } - return { - ...reduceView( - state, - viewActions.updateSort( - makeSort(sort) - ) - ), - sort, - } - - case flowActions.ADD: - return { - ...reduceView( - state, - viewActions.add( - action.item, - makeFilter(state.filter), - makeSort(state.sort) - ) - ), - } - - case flowActions.UPDATE: - return { - ...reduceView( - state, - viewActions.update( - action.item, - makeFilter(state.filter), - makeSort(state.sort) - ) - ), - } - - case flowActions.REMOVE: - return { - ...reduceView( - state, - viewActions.remove( - action.id - ) - ), - } - - case flowActions.RECEIVE: - return { - ...reduceView( - state, - viewActions.receive( - action.list, - makeFilter(state.filter), - makeSort(state.sort) - ) - ), - } - - default: - return { - ...reduceView(state, action), - } - } -} - -/** - * @public - */ -export function updateFilter(filter) { - return (dispatch, getState) => { - dispatch({ type: UPDATE_FILTER, filter, flows: getState().flows.data }) - } -} - -/** - * @public - */ -export function updateHighlight(highlight) { - return { type: UPDATE_HIGHLIGHT, highlight } -} - -/** - * @public - */ -export function updateSort(column, desc) { - return { type: UPDATE_SORT, column, desc } -} - - -/** - * @public - */ -export function selectRelative(shift) { - return (dispatch, getState) => { - let currentSelectionIndex = getState().flowView.indexOf[getState().flows.selected[0]] - let minIndex = 0 - let maxIndex = getState().flowView.data.length - 1 - let newIndex - if (currentSelectionIndex === undefined) { - newIndex = (shift < 0) ? minIndex : maxIndex - } else { - newIndex = currentSelectionIndex + shift - newIndex = Math.max(newIndex, minIndex) - newIndex = Math.min(newIndex, maxIndex) - } - let flow = getState().flowView.data[newIndex] - dispatch(flowActions.select(flow ? flow.id : undefined)) - } -} diff --git a/web/src/js/ducks/flows.js b/web/src/js/ducks/flows.js index 404db0d1..d3717533 100644 --- a/web/src/js/ducks/flows.js +++ b/web/src/js/ducks/flows.js @@ -1,53 +1,58 @@ -import { fetchApi } from '../utils' -import reduceList, * as listActions from './utils/list' -import { selectRelative } from './flowView' - -import * as msgQueueActions from './msgQueue' -import * as websocketActions from './websocket' - -export const MSG_TYPE = 'UPDATE_FLOWS' -export const DATA_URL = '/flows' - -export const ADD = 'FLOWS_ADD' -export const UPDATE = 'FLOWS_UPDATE' -export const REMOVE = 'FLOWS_REMOVE' -export const RECEIVE = 'FLOWS_RECEIVE' +import { fetchApi } from "../utils" +import reduceStore, * as storeActions from "./utils/store" +import Filt from "../filt/filt" +import { RequestUtils } from "../flow/utils" + +export const ADD = 'FLOWS_ADD' +export const UPDATE = 'FLOWS_UPDATE' +export const REMOVE = 'FLOWS_REMOVE' +export const RECEIVE = 'FLOWS_RECEIVE' +export const SELECT = 'FLOWS_SELECT' +export const SET_FILTER = 'FLOWS_SET_FILTER' +export const SET_SORT = 'FLOWS_SET_SORT' +export const SET_HIGHLIGHT = 'FLOWS_SET_HIGHLIGHT' export const REQUEST_ACTION = 'FLOWS_REQUEST_ACTION' -export const UNKNOWN_CMD = 'FLOWS_UNKNOWN_CMD' -export const FETCH_ERROR = 'FLOWS_FETCH_ERROR' -export const SELECT = 'FLOWS_SELECT' const defaultState = { + highlight: null, + filter: null, + sort: { column: null, desc: false }, selected: [], - ...reduceList(undefined, {}), + ...reduceStore(undefined, {}) } export default function reduce(state = defaultState, action) { switch (action.type) { case ADD: - return { - ...state, - ...reduceList(state, listActions.add(action.item)), - } - case UPDATE: + case REMOVE: + case RECEIVE: + // FIXME: Update state.selected on REMOVE: + // The selected flow may have been removed, we need to select the next one in the view. + let storeAction = storeActions[action.cmd]( + action.data, + makeFilter(state.filter), + makeSort(state.sort) + ) return { ...state, - ...reduceList(state, listActions.update(action.item)), + ...reduceStore(state, storeAction) } - case REMOVE: + case SET_FILTER: return { ...state, - ...reduceList(state, listActions.remove(action.id)), + filter: action.filter, + ...reduceStore(state, storeActions.setFilter(makeFilter(action.filter), makeSort(state.sort))) } - case RECEIVE: + case SET_SORT: return { ...state, - ...reduceList(state, listActions.receive(action.list)), + sort: action.sort, + ...reduceStore(state, storeActions.setSort(makeSort(action.sort))) } case SELECT: @@ -57,88 +62,133 @@ export default function reduce(state = defaultState, action) { } default: - return { - ...state, - ...reduceList(state, action), - } + return state + } +} + + +const sortKeyFuns = { + + TLSColumn: flow => flow.request.scheme, + + PathColumn: flow => RequestUtils.pretty_url(flow.request), + + MethodColumn: flow => flow.request.method, + + StatusColumn: flow => flow.response && flow.response.status_code, + + TimeColumn: flow => flow.response && flow.response.timestamp_end - flow.request.timestamp_start, + + SizeColumn: flow => { + let total = flow.request.contentLength + if (flow.response) { + total += flow.response.contentLength || 0 + } + return total + }, +} + +export function makeFilter(filter) { + if (!filter) { + return + } + return Filt.parse(filter) +} + +export function makeSort({ column, desc }) { + const sortKeyFun = sortKeyFuns[column] + if (!sortKeyFun) { + return + } + return (a, b) => { + const ka = sortKeyFun(a) + const kb = sortKeyFun(b) + if (ka > kb) { + return desc ? -1 : 1 + } + if (ka < kb) { + return desc ? 1 : -1 + } + return 0 } } -/** - * @public - */ +export function setFilter(filter) { + return { type: SET_FILTER, filter } +} + +export function setHighlight(highlight) { + return { type: SET_HIGHLIGHT, highlight } +} + +export function setSort(column, desc) { + return { type: SET_SORT, sort: { column, desc } } +} + +export function selectRelative(shift) { + return (dispatch, getState) => { + let currentSelectionIndex = getState().flows.viewIndex[getState().flows.selected[0]] + let minIndex = 0 + let maxIndex = getState().flows.view.length - 1 + let newIndex + if (currentSelectionIndex === undefined) { + newIndex = (shift < 0) ? minIndex : maxIndex + } else { + newIndex = currentSelectionIndex + shift + newIndex = window.Math.max(newIndex, minIndex) + newIndex = window.Math.min(newIndex, maxIndex) + } + let flow = getState().flows.view[newIndex] + dispatch(select(flow ? flow.id : undefined)) + } +} + + export function accept(flow) { return dispatch => fetchApi(`/flows/${flow.id}/accept`, { method: 'POST' }) } -/** - * @public - */ export function acceptAll() { return dispatch => fetchApi('/flows/accept', { method: 'POST' }) } -/** - * @public - */ export function remove(flow) { return dispatch => fetchApi(`/flows/${flow.id}`, { method: 'DELETE' }) } -/** - * @public - */ export function duplicate(flow) { return dispatch => fetchApi(`/flows/${flow.id}/duplicate`, { method: 'POST' }) } -/** - * @public - */ export function replay(flow) { return dispatch => fetchApi(`/flows/${flow.id}/replay`, { method: 'POST' }) } -/** - * @public - */ export function revert(flow) { return dispatch => fetchApi(`/flows/${flow.id}/revert`, { method: 'POST' }) } -/** - * @public - */ export function update(flow, data) { return dispatch => fetchApi.put(`/flows/${flow.id}`, data) } export function uploadContent(flow, file, type) { const body = new FormData() - file = new Blob([file], {type: 'plain/text'}) + file = new window.Blob([file], { type: 'plain/text' }) body.append('file', file) - return dispatch => fetchApi(`/flows/${flow.id}/${type}/content`, {method: 'post', body} ) + return dispatch => fetchApi(`/flows/${flow.id}/${type}/content`, { method: 'post', body }) } -/** - * @public - */ export function clear() { return dispatch => fetchApi('/clear', { method: 'POST' }) } -/** - * @public - */ export function download() { window.location = '/flows/dump' return { type: REQUEST_ACTION } } -/** - * @public - */ export function upload(file) { const body = new FormData() body.append('file', file) @@ -152,73 +202,3 @@ export function select(id) { flowIds: id ? [id] : [] } } - - -/** - * This action creater takes all WebSocket events - * - * @public websocket - */ -export function handleWsMsg(msg) { - switch (msg.cmd) { - - case websocketActions.CMD_ADD: - return addFlow(msg.data) - - case websocketActions.CMD_UPDATE: - return updateFlow(msg.data) - - case websocketActions.CMD_REMOVE: - return removeFlow(msg.data.id) - - case websocketActions.CMD_RESET: - return fetchFlows() - - default: - return { type: UNKNOWN_CMD, msg } - } -} - -/** - * @public websocket - */ -export function fetchFlows() { - return msgQueueActions.fetchData(MSG_TYPE) -} - -/** - * @public msgQueue - */ -export function receiveData(list) { - return { type: RECEIVE, list } -} - -/** - * @private - */ -export function addFlow(item) { - return { type: ADD, item } -} - -/** - * @private - */ -export function updateFlow(item) { - return { type: UPDATE, item } -} - -/** - * @private - */ -export function removeFlow(id) { - return (dispatch, getState) => { - let currentIndex = getState().flowView.indexOf[getState().flows.selected[0]] - let maxIndex = getState().flowView.data.length - 1 - let deleteLastEntry = maxIndex == 0 - if (deleteLastEntry) - dispatch(select()) - else - dispatch(selectRelative(currentIndex == maxIndex ? -1 : 1) ) - dispatch({ type: REMOVE, id }) - } -} diff --git a/web/src/js/ducks/index.js b/web/src/js/ducks/index.js index b90b24ff..753075fa 100644 --- a/web/src/js/ducks/index.js +++ b/web/src/js/ducks/index.js @@ -1,18 +1,12 @@ import { combineReducers } from 'redux' import eventLog from './eventLog' -import websocket from './websocket' import flows from './flows' -import flowView from './flowView' import settings from './settings' import ui from './ui/index' -import msgQueue from './msgQueue' export default combineReducers({ eventLog, - websocket, flows, - flowView, settings, ui, - msgQueue, }) diff --git a/web/src/js/ducks/msgQueue.js b/web/src/js/ducks/msgQueue.js deleted file mode 100644 index 6d82f4c2..00000000 --- a/web/src/js/ducks/msgQueue.js +++ /dev/null @@ -1,113 +0,0 @@ -import { fetchApi } from '../utils' -import * as websocketActions from './websocket' -import * as eventLogActions from './eventLog' -import * as flowsActions from './flows' -import * as settingsActions from './settings' - -export const INIT = 'MSG_QUEUE_INIT' -export const ENQUEUE = 'MSG_QUEUE_ENQUEUE' -export const CLEAR = 'MSG_QUEUE_CLEAR' -export const FETCH_ERROR = 'MSG_QUEUE_FETCH_ERROR' - -const handlers = { - [eventLogActions.MSG_TYPE] : eventLogActions, - [flowsActions.MSG_TYPE] : flowsActions, - [settingsActions.MSG_TYPE] : settingsActions, -} - -const defaultState = {} - -export default function reduce(state = defaultState, action) { - switch (action.type) { - - case INIT: - return { - ...state, - [action.queue]: [], - } - - case ENQUEUE: - return { - ...state, - [action.queue]: [...state[action.queue], action.msg], - } - - case CLEAR: - return { - ...state, - [action.queue]: null, - } - - default: - return state - } -} - -/** - * @public websocket - */ -export function handleWsMsg(msg) { - return (dispatch, getState) => { - const handler = handlers[msg.type] - if (msg.cmd === websocketActions.CMD_RESET) { - return dispatch(fetchData(handler.MSG_TYPE)) - } - if (getState().msgQueue[handler.MSG_TYPE]) { - return dispatch({ type: ENQUEUE, queue: handler.MSG_TYPE, msg }) - } - return dispatch(handler.handleWsMsg(msg)) - } -} - -/** - * @public - */ -export function fetchData(type) { - return dispatch => { - const handler = handlers[type] - - dispatch(init(handler.MSG_TYPE)) - - fetchApi(handler.DATA_URL) - .then(res => res.json()) - .then(json => dispatch(receive(type, json))) - .catch(error => dispatch(fetchError(type, error))) - } -} - -/** - * @private - */ -export function receive(type, res) { - return (dispatch, getState) => { - const handler = handlers[type] - const queue = getState().msgQueue[handler.MSG_TYPE] || [] - - dispatch(clear(handler.MSG_TYPE)) - dispatch(handler.receiveData(res.data)) - for (const msg of queue) { - dispatch(handler.handleWsMsg(msg)) - } - } -} - -/** - * @private - */ -export function init(queue) { - return { type: INIT, queue } -} - -/** - * @private - */ -export function clear(queue) { - return { type: CLEAR, queue } -} - -/** - * @private - */ -export function fetchError(type, error) { - return { type: FETCH_ERROR, type, error } -} diff --git a/web/src/js/ducks/settings.js b/web/src/js/ducks/settings.js index 6b21baec..a2e360de 100644 --- a/web/src/js/ducks/settings.js +++ b/web/src/js/ducks/settings.js @@ -1,12 +1,7 @@ import { fetchApi } from '../utils' -import * as websocketActions from './websocket' -import * as msgQueueActions from './msgQueue' -export const MSG_TYPE = 'UPDATE_SETTINGS' -export const DATA_URL = '/settings' - -export const RECEIVE = 'RECEIVE' -export const UPDATE = 'UPDATE' +export const RECEIVE = 'SETTINGS_RECEIVE' +export const UPDATE = 'SETTINGS_UPDATE' export const REQUEST_UPDATE = 'REQUEST_UPDATE' export const UNKNOWN_CMD = 'SETTINGS_UNKNOWN_CMD' @@ -18,12 +13,12 @@ export default function reducer(state = defaultState, action) { switch (action.type) { case RECEIVE: - return action.settings + return action.data case UPDATE: return { ...state, - ...action.settings, + ...action.data, } default: @@ -31,46 +26,7 @@ export default function reducer(state = defaultState, action) { } } -/** - * @public msgQueue - */ -export function handleWsMsg(msg) { - switch (msg.cmd) { - - case websocketActions.CMD_UPDATE: - return updateSettings(msg.data) - - default: - console.error('unknown settings update', msg) - return { type: UNKNOWN_CMD, msg } - } -} - -/** - * @public - */ export function update(settings) { fetchApi.put('/settings', settings) return { type: REQUEST_UPDATE } } - -/** - * @public websocket - */ -export function fetchData() { - return msgQueueActions.fetchData(MSG_TYPE) -} - -/** - * @public msgQueue - */ -export function receiveData(settings) { - return { type: RECEIVE, settings } -} - -/** - * @private - */ -export function updateSettings(settings) { - return { type: UPDATE, settings } -} diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js index 4a6d64cd..b5f6f78b 100644 --- a/web/src/js/ducks/ui/flow.js +++ b/web/src/js/ducks/ui/flow.js @@ -60,7 +60,7 @@ export default function reducer(state = defaultState, action) { // There is no explicit "stop edit" event. // We stop editing when we receive an update for // the currently edited flow from the server - if (action.item.id === state.modifiedFlow.id) { + if (action.data.id === state.modifiedFlow.id) { return { ...state, modifiedFlow: false, diff --git a/web/src/js/ducks/ui/index.js b/web/src/js/ducks/ui/index.js index f3c5f59e..1d989eb1 100644 --- a/web/src/js/ducks/ui/index.js +++ b/web/src/js/ducks/ui/index.js @@ -2,6 +2,7 @@ import { combineReducers } from 'redux' import flow from './flow' import header from './header' +// TODO: Just move ducks/ui/* into ducks/? export default combineReducers({ flow, header, diff --git a/web/src/js/ducks/ui/keyboard.js b/web/src/js/ducks/ui/keyboard.js index 10c69853..7418eca9 100644 --- a/web/src/js/ducks/ui/keyboard.js +++ b/web/src/js/ducks/ui/keyboard.js @@ -1,5 +1,4 @@ import { Key } from '../../utils' -import { selectRelative as selectFlowRelative } from '../flowView' import { selectTab } from './flow' import * as flowsActions from '../flows' @@ -20,29 +19,29 @@ export function onKeyDown(e) { switch (key) { case Key.K: case Key.UP: - dispatch(selectFlowRelative(-1)) + dispatch(flowsActions.selectRelative(-1)) break case Key.J: case Key.DOWN: - dispatch(selectFlowRelative(+1)) + dispatch(flowsActions.selectRelative(+1)) break case Key.SPACE: case Key.PAGE_DOWN: - dispatch(selectFlowRelative(+10)) + dispatch(flowsActions.selectRelative(+10)) break case Key.PAGE_UP: - dispatch(selectFlowRelative(-10)) + dispatch(flowsActions.selectRelative(-10)) break case Key.END: - dispatch(selectFlowRelative(+1e10)) + dispatch(flowsActions.selectRelative(+1e10)) break case Key.HOME: - dispatch(selectFlowRelative(-1e10)) + dispatch(flowsActions.selectRelative(-1e10)) break case Key.ESC: diff --git a/web/src/js/ducks/utils/list.js b/web/src/js/ducks/utils/list.js deleted file mode 100644 index fdeb5856..00000000 --- a/web/src/js/ducks/utils/list.js +++ /dev/null @@ -1,105 +0,0 @@ -import _ from 'lodash' - -export const ADD = 'LIST_ADD' -export const UPDATE = 'LIST_UPDATE' -export const REMOVE = 'LIST_REMOVE' -export const RECEIVE = 'LIST_RECEIVE' - -const defaultState = { - data: [], - byId: {}, - indexOf: {}, -} - -export default function reduce(state = defaultState, action) { - switch (action.type) { - - case ADD: - return { - ...state, - data: [...state.data, action.item], - byId: { ...state.byId, [action.item.id]: action.item }, - indexOf: { ...state.indexOf, [action.item.id]: state.data.length }, - } - - case UPDATE: { - const index = state.indexOf[action.item.id] - - if (index == null) { - return state - } - - const data = [...state.data] - - data[index] = action.item - - return { - ...state, - data, - byId: { ...state.byId, [action.item.id]: action.item } - } - } - - case REMOVE: { - const index = state.indexOf[action.id] - - if (index == null) { - return state - } - - const data = [...state.data] - const indexOf = { ...state.indexOf, [action.id]: null } - - data.splice(index, 1) - for (let i = data.length - 1; i >= index; i--) { - indexOf[data[i].id] = i - } - - return { - ...state, - data, - indexOf, - byId: { ...state.byId, [action.id]: null }, - } - } - - case RECEIVE: - return { - ...state, - data: action.list, - byId: _.fromPairs(action.list.map(item => [item.id, item])), - indexOf: _.fromPairs(action.list.map((item, index) => [item.id, index])), - } - - default: - return state - } -} - -/** - * @public - */ -export function add(item) { - return { type: ADD, item } -} - -/** - * @public - */ -export function update(item) { - return { type: UPDATE, item } -} - -/** - * @public - */ -export function remove(id) { - return { type: REMOVE, id } -} - -/** - * @public - */ -export function receive(list) { - return { type: RECEIVE, list } -} diff --git a/web/src/js/ducks/utils/store.js b/web/src/js/ducks/utils/store.js new file mode 100644 index 00000000..9ea4f02e --- /dev/null +++ b/web/src/js/ducks/utils/store.js @@ -0,0 +1,210 @@ +export const SET_FILTER = 'LIST_SET_FILTER' +export const SET_SORT = 'LIST_SET_SORT' +export const ADD = 'LIST_ADD' +export const UPDATE = 'LIST_UPDATE' +export const REMOVE = 'LIST_REMOVE' +export const RECEIVE = 'LIST_RECEIVE' + +const defaultState = { + byId: {}, + list: [], + listIndex: {}, + view: [], + viewIndex: {}, +} + +/** + * The store reducer can be used as a mixin to another reducer that always returns a + * new { byId, list, listIndex, view, viewIndex } object. The reducer using the store + * usually has to map its action to the matching store action and then call the mixin with that. + * + * Example Usage: + * + * import reduceStore, * as storeActions from "./utils/store" + * + * case EVENTLOG_ADD: + * return { + * ...state, + * ...reduceStore(state, storeActions.add(action.data)) + * } + * + */ +export default function reduce(state = defaultState, action) { + + let { byId, list, listIndex, view, viewIndex } = state + + switch (action.type) { + case SET_FILTER: + view = list.filter(action.filter).sort(action.sort) + viewIndex = {} + view.forEach((item, index) => { + viewIndex[item.id] = index + }) + break + + case SET_SORT: + view = [...view].sort(action.sort) + viewIndex = {} + view.forEach((item, index) => { + viewIndex[item.id] = index + }) + break + + case ADD: + if (action.item.id in byId) { + // we already had that. + break + } + byId = { ...byId, [action.item.id]: action.item } + listIndex = { ...listIndex, [action.item.id]: list.length } + list = [...list, action.item] + if (action.filter(action.item)) { + ({ view, viewIndex } = sortedInsert(state, action.item, action.sort)) + } + break + + case UPDATE: + byId = { ...byId, [action.item.id]: action.item } + list = [...list] + list[listIndex[action.item.id]] = action.item + + let hasOldItem = action.item.id in viewIndex + let hasNewItem = action.filter(action.item) + if (hasNewItem && !hasOldItem) { + ({view, viewIndex} = sortedInsert(state, action.item, action.sort)) + } + else if (!hasNewItem && hasOldItem) { + ({data: view, dataIndex: viewIndex} = removeData(view, viewIndex, action.item.id)) + } + else if (hasNewItem && hasOldItem) { + ({view, viewIndex} = sortedUpdate(state, action.item, action.sort)) + } + break + + case REMOVE: + if (!(action.id in byId)) { + break + } + delete byId[action.id]; + ({data: list, dataIndex: listIndex} = removeData(list, listIndex, action.id)) + + if (action.id in viewIndex) { + ({data: view, dataIndex: viewIndex} = removeData(view, viewIndex, action.id)) + } + break + + case RECEIVE: + list = action.list + listIndex = {} + byId = {} + list.forEach((item, i) => { + byId[item.id] = item + listIndex[item.id] = i + }) + view = list.filter(action.filter).sort(action.sort) + viewIndex = {} + view.forEach((item, index) => { + viewIndex[item.id] = index + }) + break + } + return { byId, list, listIndex, view, viewIndex } +} + + +export function setFilter(filter = defaultFilter, sort = defaultSort) { + return { type: SET_FILTER, filter, sort } +} + +export function setSort(sort = defaultSort) { + return { type: SET_SORT, sort } +} + +export function add(item, filter = defaultFilter, sort = defaultSort) { + return { type: ADD, item, filter, sort } +} + +export function update(item, filter = defaultFilter, sort = defaultSort) { + return { type: UPDATE, item, filter, sort } +} + +export function remove(id) { + return { type: REMOVE, id } +} + +export function receive(list, filter = defaultFilter, sort = defaultSort) { + return { type: RECEIVE, list, filter, sort } +} + +function sortedInsert(state, item, sort) { + const index = sortedIndex(state.view, item, sort) + const view = [...state.view] + const viewIndex = { ...state.viewIndex } + + view.splice(index, 0, item) + for (let i = view.length - 1; i >= index; i--) { + viewIndex[view[i].id] = i + } + + return { view, viewIndex } +} + +function removeData(currentData, currentDataIndex, id) { + const index = currentDataIndex[id] + const data = [...currentData] + const dataIndex = { ...currentDataIndex } + delete dataIndex[id]; + + data.splice(index, 1) + for (let i = data.length - 1; i >= index; i--) { + dataIndex[data[i].id] = i + } + + return { data, dataIndex } +} + +function sortedUpdate(state, item, sort) { + let view = [...state.view] + let viewIndex = { ...state.viewIndex } + let index = viewIndex[item.id] + view[index] = item + while (index + 1 < view.length && sort(view[index], view[index + 1]) > 0) { + view[index] = view[index + 1] + view[index + 1] = item + viewIndex[item.id] = index + 1 + viewIndex[view[index].id] = index + ++index + } + while (index > 0 && sort(view[index], view[index - 1]) < 0) { + view[index] = view[index - 1] + view[index - 1] = item + viewIndex[item.id] = index - 1 + viewIndex[view[index].id] = index + --index + } + return { view, viewIndex } +} + +function sortedIndex(list, item, sort) { + let low = 0 + let high = list.length + + while (low < high) { + const middle = (low + high) >>> 1 + if (sort(item, list[middle]) >= 0) { + low = middle + 1 + } else { + high = middle + } + } + + return low +} + +function defaultFilter() { + return true +} + +function defaultSort(a, b) { + return 0 +} diff --git a/web/src/js/ducks/utils/view.js b/web/src/js/ducks/utils/view.js deleted file mode 100755 index 6bf0a63e..00000000 --- a/web/src/js/ducks/utils/view.js +++ /dev/null @@ -1,189 +0,0 @@ -import _ from 'lodash' - -export const UPDATE_FILTER = 'VIEW_UPDATE_FILTER' -export const UPDATE_SORT = 'VIEW_UPDATE_SORT' -export const ADD = 'VIEW_ADD' -export const UPDATE = 'VIEW_UPDATE' -export const REMOVE = 'VIEW_REMOVE' -export const RECEIVE = 'VIEW_RECEIVE' - -const defaultState = { - data: [], - indexOf: {}, -} - -export default function reduce(state = defaultState, action) { - switch (action.type) { - - case UPDATE_FILTER: - { - const data = action.list.filter(action.filter).sort(action.sort) - return { - ...state, - data, - indexOf: _.fromPairs(data.map((item, index) => [item.id, index])), - } - } - - case UPDATE_SORT: - { - const data = [...state.data].sort(action.sort) - return { - ...state, - data, - indexOf: _.fromPairs(data.map((item, index) => [item.id, index])), - } - } - - case ADD: - if (state.indexOf[action.item.id] != null || !action.filter(action.item)) { - return state - } - return { - ...state, - ...sortedInsert(state, action.item, action.sort), - } - - case REMOVE: - if (state.indexOf[action.id] == null) { - return state - } - return { - ...state, - ...sortedRemove(state, action.id), - } - - case UPDATE: - let hasOldItem = state.indexOf[action.item.id] !== null && state.indexOf[action.item.id] !== undefined - let hasNewItem = action.filter(action.item) - if (!hasNewItem && !hasOldItem) { - return state - } - if (hasNewItem && !hasOldItem) { - return { - ...state, - ...sortedInsert(state, action.item, action.sort) - } - } - if (!hasNewItem && hasOldItem) { - return { - ...state, - ...sortedRemove(state, action.item.id) - } - } - if (hasNewItem && hasOldItem) { - return { - ...state, - ...sortedUpdate(state, action.item, action.sort), - } - } - case RECEIVE: - { - const data = action.list.filter(action.filter).sort(action.sort) - return { - ...state, - data, - indexOf: _.fromPairs(data.map((item, index) => [item.id, index])), - } - } - - default: - return state - } -} - -export function updateFilter(list, filter = defaultFilter, sort = defaultSort) { - return { type: UPDATE_FILTER, list, filter, sort } -} - -export function updateSort(sort = defaultSort) { - return { type: UPDATE_SORT, sort } -} - -export function add(item, filter = defaultFilter, sort = defaultSort) { - return { type: ADD, item, filter, sort } -} - -export function update(item, filter = defaultFilter, sort = defaultSort) { - return { type: UPDATE, item, filter, sort } -} - -export function remove(id) { - return { type: REMOVE, id } -} - -export function receive(list, filter = defaultFilter, sort = defaultSort) { - return { type: RECEIVE, list, filter, sort } -} - -function sortedInsert(state, item, sort) { - const index = sortedIndex(state.data, item, sort) - const data = [ ...state.data ] - const indexOf = { ...state.indexOf } - - data.splice(index, 0, item) - for (let i = data.length - 1; i >= index; i--) { - indexOf[data[i].id] = i - } - - return { data, indexOf } -} - -function sortedRemove(state, id) { - const index = state.indexOf[id] - const data = [...state.data] - const indexOf = { ...state.indexOf, [id]: null } - - data.splice(index, 1) - for (let i = data.length - 1; i >= index; i--) { - indexOf[data[i].id] = i - } - - return { data, indexOf } -} - -function sortedUpdate(state, item, sort) { - let data = [ ...state.data ] - let indexOf = { ...state.indexOf } - let index = indexOf[item.id] - data[index] = item - while (index + 1 < data.length && sort(data[index], data[index + 1]) > 0) { - data[index] = data[index + 1] - data[index + 1] = item - indexOf[item.id] = index + 1 - indexOf[data[index].id] = index - ++index - } - while (index > 0 && sort(data[index], data[index - 1]) < 0) { - data[index] = data[index - 1] - data[index - 1] = item - indexOf[item.id] = index - 1 - indexOf[data[index].id] = index - --index - } - return { data, indexOf } -} - -function sortedIndex(list, item, sort) { - let low = 0 - let high = list.length - - while (low < high) { - const middle = (low + high) >>> 1 - if (sort(item, list[middle]) >= 0) { - low = middle + 1 - } else { - high = middle - } - } - - return low -} - -function defaultFilter() { - return true -} - -function defaultSort(a, b) { - return 0 -} diff --git a/web/src/js/ducks/websocket.js b/web/src/js/ducks/websocket.js deleted file mode 100644 index 21400bb5..00000000 --- a/web/src/js/ducks/websocket.js +++ /dev/null @@ -1,93 +0,0 @@ -import { ConnectionActions } from '../actions.js' -import { AppDispatcher } from '../dispatcher.js' - -import * as msgQueueActions from './msgQueue' -import * as eventLogActions from './eventLog' -import * as flowsActions from './flows' -import * as settingsActions from './settings' - -export const CMD_ADD = 'add' -export const CMD_UPDATE = 'update' -export const CMD_REMOVE = 'remove' -export const CMD_RESET = 'reset' - -export const SYM_SOCKET = Symbol('WEBSOCKET_SYM_SOCKET') - -export const CONNECT = 'WEBSOCKET_CONNECT' -export const CONNECTED = 'WEBSOCKET_CONNECTED' -export const DISCONNECT = 'WEBSOCKET_DISCONNECT' -export const DISCONNECTED = 'WEBSOCKET_DISCONNECTED' -export const ERROR = 'WEBSOCKET_ERROR' -export const MESSAGE = 'WEBSOCKET_MESSAGE' - -/* we may want to have an error message attribute here at some point */ -const defaultState = { connected: false, socket: null } - -export default function reduce(state = defaultState, action) { - switch (action.type) { - - case CONNECT: - return { ...state, [SYM_SOCKET]: action.socket } - - case CONNECTED: - return { ...state, connected: true } - - case DISCONNECT: - return { ...state, connected: false } - - case DISCONNECTED: - return { ...state, [SYM_SOCKET]: null, connected: false } - - default: - return state - } -} - -export function connect() { - return dispatch => { - const socket = new WebSocket(location.origin.replace('http', 'ws') + '/updates') - - socket.addEventListener('open', () => dispatch(onConnect())) - socket.addEventListener('close', () => dispatch(onDisconnect())) - socket.addEventListener('message', msg => dispatch(onMessage(JSON.parse(msg.data)))) - socket.addEventListener('error', error => dispatch(onError(error))) - - dispatch({ type: CONNECT, socket }) - } -} - -export function disconnect() { - return (dispatch, getState) => { - getState().settings[SYM_SOCKET].close() - dispatch({ type: DISCONNECT }) - } -} - -export function onConnect() { - // workaround to make sure that our state is already available. - return dispatch => { - dispatch({ type: CONNECTED }) - dispatch(settingsActions.fetchData()) - dispatch(flowsActions.fetchFlows()) - dispatch(eventLogActions.fetchData()) - } -} - -export function onMessage(msg) { - return msgQueueActions.handleWsMsg(msg) -} - -export function onDisconnect() { - return dispatch => { - dispatch(eventLogActions.add('WebSocket connection closed.')) - dispatch({ type: DISCONNECTED }) - } -} - -export function onError(error) { - // @todo let event log subscribe WebSocketActions.ERROR - return dispatch => { - dispatch(eventLogActions.add('WebSocket connection error.')) - dispatch({ type: ERROR, error }) - } -} |