diff options
-rw-r--r-- | web/src/js/__tests__/ducks/ui.js | 4 | ||||
-rw-r--r-- | web/src/js/actions.js | 55 | ||||
-rw-r--r-- | web/src/js/app.jsx | 4 | ||||
-rw-r--r-- | web/src/js/components/EventLog.jsx | 6 | ||||
-rw-r--r-- | web/src/js/components/FlowTable/FlowTableHead.jsx | 6 | ||||
-rw-r--r-- | web/src/js/components/Header.jsx | 1 | ||||
-rw-r--r-- | web/src/js/components/Header/ViewMenu.jsx | 4 | ||||
-rw-r--r-- | web/src/js/components/MainView.jsx | 10 | ||||
-rw-r--r-- | web/src/js/connection.js | 16 | ||||
-rw-r--r-- | web/src/js/ducks/eventLog.js | 162 | ||||
-rw-r--r-- | web/src/js/ducks/flows.js | 282 | ||||
-rw-r--r-- | web/src/js/ducks/utils/list.js | 262 | ||||
-rw-r--r-- | web/src/js/ducks/utils/view.js | 134 | ||||
-rw-r--r-- | web/src/js/ducks/websocket.js | 4 |
14 files changed, 471 insertions, 479 deletions
diff --git a/web/src/js/__tests__/ducks/ui.js b/web/src/js/__tests__/ducks/ui.js index 81ae852c..3cf3afc1 100644 --- a/web/src/js/__tests__/ducks/ui.js +++ b/web/src/js/__tests__/ducks/ui.js @@ -1,8 +1,8 @@ jest.unmock("../../ducks/ui"); jest.unmock("../../ducks/flows"); -import reducer, {setActiveMenu} from '../../ducks/ui'; -import {SELECT_FLOW} from '../../ducks/flows'; +import reducer, { setActiveMenu } from '../../ducks/ui'; +import { SELECT_FLOW } from '../../ducks/flows'; describe("ui reducer", () => { it("should return the initial state", () => { diff --git a/web/src/js/actions.js b/web/src/js/actions.js index bb1d0dd6..e00e3cad 100644 --- a/web/src/js/actions.js +++ b/web/src/js/actions.js @@ -39,61 +39,6 @@ export var ConnectionActions = { } }; -export var FlowActions = { - accept: function (flow) { - $.post("/flows/" + flow.id + "/accept"); - }, - accept_all: function(){ - $.post("/flows/accept"); - }, - "delete": function(flow){ - $.ajax({ - type:"DELETE", - url: "/flows/" + flow.id - }); - }, - duplicate: function(flow){ - $.post("/flows/" + flow.id + "/duplicate"); - }, - replay: function(flow){ - $.post("/flows/" + flow.id + "/replay"); - }, - revert: function(flow){ - $.post("/flows/" + flow.id + "/revert"); - }, - update: function (flow, nextProps) { - /* - //Facebook Flux: We do an optimistic update on the client already. - var nextFlow = _.cloneDeep(flow); - _.merge(nextFlow, nextProps); - AppDispatcher.dispatchViewAction({ - type: ActionTypes.FLOW_STORE, - cmd: StoreCmds.UPDATE, - data: nextFlow - }); - */ - $.ajax({ - type: "PUT", - url: "/flows/" + flow.id, - contentType: 'application/json', - data: JSON.stringify(nextProps) - }); - }, - clear: function(){ - $.post("/clear"); - }, - download: () => window.location = "/flows/dump", - - upload: (file) => { - let data = new FormData(); - data.append('file', file); - fetchApi("/flows/dump", { - method: 'post', - body: data - }) - } -}; - export var Query = { SEARCH: "s", HIGHLIGHT: "h", diff --git a/web/src/js/app.jsx b/web/src/js/app.jsx index 8fa52a00..1291df7a 100644 --- a/web/src/js/app.jsx +++ b/web/src/js/app.jsx @@ -10,7 +10,7 @@ import Connection from "./connection" import ProxyApp from "./components/ProxyApp" import MainView from './components/MainView' import rootReducer from './ducks/index' -import { addLogEntry } from "./ducks/eventLog" +import { add as addLog } from "./ducks/eventLog" // logger must be last const store = createStore( @@ -19,7 +19,7 @@ const store = createStore( ) window.addEventListener('error', msg => { - store.dispatch(addLogEntry(msg)) + store.dispatch(addLog(msg)) }) // @todo remove this diff --git a/web/src/js/components/EventLog.jsx b/web/src/js/components/EventLog.jsx index 169162ee..1072b124 100644 --- a/web/src/js/components/EventLog.jsx +++ b/web/src/js/components/EventLog.jsx @@ -1,6 +1,6 @@ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' -import { toggleEventLogFilter, toggleEventLogVisibility } from '../ducks/eventLog' +import { toggleFilter, toggleVisibility } from '../ducks/eventLog' import ToggleButton from './common/ToggleButton' import EventList from './EventLog/EventList' @@ -73,7 +73,7 @@ export default connect( events: state.eventLog.filteredEvents, }), { - onClose: toggleEventLogVisibility, - onToggleFilter: toggleEventLogFilter, + onClose: toggleVisibility, + onToggleFilter: toggleFilter, } )(EventLog) diff --git a/web/src/js/components/FlowTable/FlowTableHead.jsx b/web/src/js/components/FlowTable/FlowTableHead.jsx index 840f6a34..afbd7906 100644 --- a/web/src/js/components/FlowTable/FlowTableHead.jsx +++ b/web/src/js/components/FlowTable/FlowTableHead.jsx @@ -3,7 +3,7 @@ import { connect } from 'react-redux' import classnames from 'classnames' import columns from './FlowColumns' -import { setSort } from "../../ducks/flows" +import { updateSorter } from "../../ducks/flows" FlowTableHead.propTypes = { onSort: PropTypes.func.isRequired, @@ -19,7 +19,7 @@ function FlowTableHead({ sortColumn, sortDesc, onSort }) { {columns.map(Column => ( <th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)} key={Column.name} - onClick={() => onSort({ sortColumn: Column.name, sortDesc: Column.name !== sortColumn ? false : !sortDesc })}> + onClick={() => onSort(Column.name, Column.name !== sortColumn ? false : !sortDesc, Column.sortKeyFun)}> {Column.headerName} </th> ))} @@ -33,6 +33,6 @@ export default connect( sortColumn: state.flows.sort.sortColumn, }), { - onSort: setSort, + onSort: updateSorter, } )(FlowTableHead) diff --git a/web/src/js/components/Header.jsx b/web/src/js/components/Header.jsx index ab25eb41..5ebe8c7e 100644 --- a/web/src/js/components/Header.jsx +++ b/web/src/js/components/Header.jsx @@ -1,7 +1,6 @@ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' import classnames from 'classnames' -import { toggleEventLogVisibility } from '../ducks/eventLog' import MainMenu from './Header/MainMenu' import ViewMenu from './Header/ViewMenu' import OptionMenu from './Header/OptionMenu' diff --git a/web/src/js/components/Header/ViewMenu.jsx b/web/src/js/components/Header/ViewMenu.jsx index 8d662c28..894fa71a 100644 --- a/web/src/js/components/Header/ViewMenu.jsx +++ b/web/src/js/components/Header/ViewMenu.jsx @@ -1,7 +1,7 @@ import React, { PropTypes } from 'react' import { connect } from 'react-redux' import ToggleButton from '../common/ToggleButton' -import { toggleEventLogVisibility } from '../../ducks/eventLog' +import { toggleVisibility } from '../../ducks/eventLog' ViewMenu.title = 'View' ViewMenu.route = 'flows' @@ -27,6 +27,6 @@ export default connect( visible: state.eventLog.visible, }), { - onToggle: toggleEventLogVisibility, + onToggle: toggleVisibility, } )(ViewMenu) diff --git a/web/src/js/components/MainView.jsx b/web/src/js/components/MainView.jsx index 7064d3bf..a271a7f0 100644 --- a/web/src/js/components/MainView.jsx +++ b/web/src/js/components/MainView.jsx @@ -6,7 +6,7 @@ import { Key } from '../utils.js' import Splitter from './common/Splitter' import FlowTable from './FlowTable' import FlowView from './FlowView' -import { selectFlow, setFilter, setHighlight } from '../ducks/flows' +import { selectFlow, updateFilter, updateHighlight } from '../ducks/flows' class MainView extends Component { @@ -25,10 +25,10 @@ class MainView extends Component { this.props.selectFlow(nextProps.routeParams.flowId) } if (nextProps.location.query[Query.SEARCH] !== nextProps.filter) { - this.props.setFilter(nextProps.location.query[Query.SEARCH], false) + this.props.updateFilter(nextProps.location.query[Query.SEARCH], false) } if (nextProps.location.query[Query.HIGHLIGHT] !== nextProps.highlight) { - this.props.setHighlight(nextProps.location.query[Query.HIGHLIGHT], false) + this.props.updateHighlight(nextProps.location.query[Query.HIGHLIGHT], false) } } @@ -190,8 +190,8 @@ export default connect( }), { selectFlow, - setFilter, - setHighlight, + updateFilter, + updateHighlight, }, undefined, { withRef: true } diff --git a/web/src/js/connection.js b/web/src/js/connection.js index 524a8f6a..6292cd57 100644 --- a/web/src/js/connection.js +++ b/web/src/js/connection.js @@ -14,22 +14,22 @@ export default function Connection(url, dispatch) { ws.onopen = function () { dispatch(webSocketActions.connected()) dispatch(settingsActions.fetchSettings()) - dispatch(flowActions.fetchFlows()) + dispatch(eventLogActions.fetchData()) + dispatch(flowActions.fetchData()) // workaround to make sure that our state is already available. .then(() => { console.log("flows are loaded now") ConnectionActions.open() }) - dispatch(eventLogActions.fetchLogEntries()) }; ws.onmessage = function (m) { var message = JSON.parse(m.data); AppDispatcher.dispatchServerAction(message); switch (message.type) { - case eventLogActions.UPDATE_LOG: - return dispatch(eventLogActions.updateLogEntries(message)) - case flowActions.UPDATE_FLOWS: - return dispatch(flowActions.updateFlows(message)) + case eventLogActions.WS_MSG_TYPE: + return dispatch(eventLogActions.handleWsMsg(message)) + case flowActions.WS_MSG_TYPE: + return dispatch(flowActions.handleWsMsg(message)) case settingsActions.UPDATE_SETTINGS: return dispatch(settingsActions.handleWsMsg(message)) default: @@ -38,11 +38,11 @@ export default function Connection(url, dispatch) { }; ws.onerror = function () { ConnectionActions.error(); - dispatch(eventLogActions.addLogEntry("WebSocket connection error.")); + dispatch(eventLogActions.add("WebSocket connection error.")); }; ws.onclose = function () { ConnectionActions.close(); - dispatch(eventLogActions.addLogEntry("WebSocket connection closed.")); + dispatch(eventLogActions.add("WebSocket connection closed.")); dispatch(webSocketActions.disconnected()); }; return ws; diff --git a/web/src/js/ducks/eventLog.js b/web/src/js/ducks/eventLog.js index 44b67b2c..1c9d217c 100644 --- a/web/src/js/ducks/eventLog.js +++ b/web/src/js/ducks/eventLog.js @@ -1,80 +1,138 @@ -import makeList from "./utils/list" -import {updateViewFilter, updateViewList} from "./utils/view" +import { fetchApi as fetch } from '../utils' +import { CMD_RESET as WS_CMD_RESET } from './websocket' +import reduceList, * as listActions from './utils/list' -const TOGGLE_FILTER = 'TOGGLE_EVENTLOG_FILTER' -const TOGGLE_VISIBILITY = 'TOGGLE_EVENTLOG_VISIBILITY' -export const UPDATE_LOG = "UPDATE_EVENTLOG" - -const { - reduceList, - updateList, - fetchList, - addItem, -} = makeList(UPDATE_LOG, "/events") +export const WS_MSG_TYPE = 'UPDATE_LOG' +export const TOGGLE_VISIBILITY = 'EVENTLOG_TOGGLE_VISIBILITY' +export const TOGGLE_FILTER = 'EVENTLOG_TOGGLE_FILTER' +export const ADD = 'EVENTLOG_ADD' +export const WS_MSG = 'EVENTLOG_WS_MSG' +export const REQUEST = 'EVENTLOG_REQUEST' +export const RECEIVE = 'EVENTLOG_RECEIVE' +export const FETCH_ERROR = 'EVENTLOG_FETCH_ERROR' const defaultState = { + logId: 0, visible: false, - filter: { - "debug": false, - "info": true, - "web": true - }, - events: reduceList(), - filteredEvents: [], + filters: { debug: false, info: true, web: true }, + list: reduceList(undefined, { type: Symbol('EVENTLOG_INIT_LIST') }) } -export default function reducer(state = defaultState, action) { +export default function reduce(state = defaultState, action) { switch (action.type) { + + case TOGGLE_VISIBILITY: + return { ...state, visible: !state.visible } + case TOGGLE_FILTER: - const filter = { - ...state.filter, - [action.filter]: !state.filter[action.filter] + const filters = { ...state.filters, [action.filter]: !state.filters[action.filter] } + return { + ...state, + filters, + list: reduceList(state.list, listActions.updateFilter(e => filters[e.level])) } + + case ADD: return { ...state, - filter, - filteredEvents: updateViewFilter( - state.events, - x => filter[x.level] - ) + logId: state.logId + 1, + list: reduceList(state.list, listActions.add({ + id: `log-${state.logId}`, + message: action.message, + level: action.level, + })) } - case TOGGLE_VISIBILITY: + + case WS_MSG: return { ...state, - visible: !state.visible + list: reduceList(state.list, listActions.handleWsMsg(action.msg)) } - case UPDATE_LOG: - const events = reduceList(state.events, action) + + case REQUEST: return { ...state, - events, - filteredEvents: updateViewList( - state.filteredEvents, - state.events, - events, - action, - x => state.filter[x.level] - ) + list: reduceList(state.list, listActions.request()) } + + case RECEIVE: + return { + ...state, + list: reduceList(state.list, listActions.receive(action.list)) + } + default: return state } } +/** + * @public + */ +export function toggleFilter(filter) { + return { type: TOGGLE_FILTER, filter } +} + +/** + * @public + * + * @todo move to ui? + */ +export function toggleVisibility() { + return { type: TOGGLE_VISIBILITY } +} + +/** + * @public + */ +export function add(message, level = 'web') { + return { type: ADD, message, level } +} + +/** + * This action creater takes all WebSocket events + * + * @public websocket + */ +export function handleWsMsg(msg) { + if (msg.cmd === WS_CMD_RESET) { + return fetchData() + } + return { type: WS_MSG, msg } +} + +/** + * @private + */ +export function fetchData() { + return dispatch => { + dispatch(request()) -export function toggleEventLogFilter(filter) { - return {type: TOGGLE_FILTER, filter} + return fetch('/events') + .then(res => res.json()) + .then(json => dispatch(receive(json.data))) + .catch(error => dispatch(fetchError(error))) + } } -export function toggleEventLogVisibility() { - return {type: TOGGLE_VISIBILITY} + +/** + * @private + */ +export function request() { + return { type: REQUEST } } -let id = 0 -export function addLogEntry(message, level = "web") { - return addItem({ - message, - level, - id: `log-${id++}` - }) + +/** + * @private + */ +export function receive(list) { + return { type: RECEIVE, list } +} + +/** + * @private + */ +export function fetchError(error) { + return { type: FETCH_ERROR, error } } -export {updateList as updateLogEntries, fetchList as fetchLogEntries}
\ No newline at end of file diff --git a/web/src/js/ducks/flows.js b/web/src/js/ducks/flows.js index b877d3e4..aad82de2 100644 --- a/web/src/js/ducks/flows.js +++ b/web/src/js/ducks/flows.js @@ -1,114 +1,250 @@ -import makeList from "./utils/list" -import Filt from "../filt/filt" -import {updateViewFilter, updateViewList, updateViewSort} from "./utils/view" -import {reverseString} from "../utils.js"; -import * as columns from "../components/FlowTable/FlowColumns"; +import { fetchApi as fetch } from '../utils' +import { CMD_RESET as WS_CMD_RESET } from './websocket' +import reduceList, * as listActions from './utils/list' -export const UPDATE_FLOWS = "UPDATE_FLOWS" -export const SET_FILTER = "SET_FLOW_FILTER" -export const SET_HIGHLIGHT = "SET_FLOW_HIGHLIGHT" -export const SET_SORT = "SET_FLOW_SORT" -export const SELECT_FLOW = "SELECT_FLOW" - -const { - reduceList, - updateList, - fetchList, -} = makeList(UPDATE_FLOWS, "/flows") +export const WS_MSG_TYPE ='UPDATE_FLOWS' +export const UPDATE_FILTER = 'FLOWS_UPDATE_FLOW_FILTER' +export const UPDATE_HIGHLIGHT = 'FLOWS_UPDATE_FLOW_HIGHLIGHT' +export const UPDATE_SORT = 'FLOWS_UPDATE_FLOW_SORT' +export const WS_MSG = 'FLOWS_WS_MSG' +export const SELECT_FLOW = 'FLOWS_SELECT_FLOW' +export const REQUEST_ACTION = 'FLOWS_REQUEST_ACTION' +export const REQUEST = 'FLOWS_REQUEST' +export const RECEIVE = 'FLOWS_RECEIVE' +export const FETCH_ERROR = 'FLOWS_FETCH_ERROR' const defaultState = { - all: reduceList(), selected: [], - view: [], - filter: undefined, - highlight: undefined, - sort: {sortColumn: undefined, sortDesc: false}, -} - -function makeFilterFn(filter) { - return filter ? Filt.parse(filter) : () => true; + sorter: {}, + filter: null, + highlight: null, + list: reduceList(undefined, { type: Symbol('FLOWS_INIT_LIST') }), } +export default function reduce(state = defaultState, action) { + switch (action.type) { -function makeSortFn(sort){ - let column = columns[sort.sortColumn]; - if (!column) return; + case UPDATE_FILTER: + return { + ...state, + filter: action.filter, + list: reduceList(state.list, listActions.updateFilter(makeFilterFun(action.filter))), + } - let sortKeyFun = column.sortKeyFun; - if (sort.sortDesc) { - sortKeyFun = sortKeyFun && function (flow) { - const k = column.sortKeyFun(flow); - return _.isString(k) ? reverseString("" + k) : -k; - }; - } - return sortKeyFun; -} + case UPDATE_HIGHLIGHT: + return { + ...state, + highlight: action.highlight, + } -export default function reducer(state = defaultState, action) { - switch (action.type) { - case UPDATE_FLOWS: - let all = reduceList(state.all, action) + case UPDATE_SORTER: return { ...state, - all, - view: updateViewList(state.view, state.all, all, action, makeFilterFn(action.filter), makeSortFn(state.sort)) + sorter: { column: action.column, desc: action.desc }, + list: reduceList(state.list, listActions.updateSorter(makeSortFun(action.sortKeyFun, action.desc))), } - case SET_FILTER: + + case SELECT_FLOW: return { ...state, - filter: action.filter, - view: updateViewFilter(state.all, makeFilterFn(action.filter), makeSortFn(state.sort)) + selected: [action.id], } - case SET_HIGHLIGHT: + + case WS_MSG: return { ...state, - highlight: action.highlight + list: reduceList(state.list, listActions.handleWsMsg(action.msg)), } - case SET_SORT: + + case REQUEST: return { ...state, - sort: action.sort, - view: updateViewSort(state.view, makeSortFn(action.sort)) + list: reduceList(state.list, listActions.request()) } - case SELECT_FLOW: + + case RECEIVE: return { ...state, - selected: [action.flowId] + list: reduceList(state.list, listActions.receive(action.list)) } + default: return state } } +function makeFilterFun(filter) { + return filter ? Filt.parse(filter) : () => true +} -export function setFilter(filter) { - return { - type: SET_FILTER, - filter +function makeSortFun(sortKeyFun, desc) { + 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 } } -export function setHighlight(highlight) { - return { - type: SET_HIGHLIGHT, - highlight + +/** + * @public + */ +export function updateFilter(filter) { + return { type: UPDATE_FILTER, filter } +} + +/** + * @public + */ +export function updateHighlight(highlight) { + return { type: UPDATE_HIGHLIGHT, highlight } +} + +/** + * @public + */ +export function updateSorter(column, desc, sortKeyFun) { + return { type: UPDATE_SORTER, column, desc, sortKeyFun } +} + +/** + * @public + */ +export function selectFlow(id) { + return (dispatch, getState) => { + dispatch({ type: SELECT_FLOW, currentSelection: getState().flows.selected[0], id }) } } -export function setSort(sort){ - return { - type: SET_SORT, - sort + +/** + * @public websocket + */ +export function handleWsMsg(msg) { + if (msg.cmd === WS_CMD_RESET) { + return fetchData() } + return { type: WS_MSG, msg } } -export function selectFlow(flowId) { - return (dispatch, getState) => { - dispatch({ - type: SELECT_FLOW, - currentSelection: getState().flows.selected[0], - flowId - }) + +/** + * @public websocket + */ +export function fetchData() { + return dispatch => { + dispatch(request()) + + return fetch('/flows') + .then(res => res.json()) + .then(json => dispatch(receive(json.data))) + .catch(error => dispatch(fetchError(error))) } } +/** + * @public + */ +export function accept(flow) { + fetch(`/flows/${flow.id}/accept`, { method: 'POST' }) + return { type: REQUEST_ACTION } +} + +/** + * @public + */ +export function acceptAll() { + fetch('/flows/accept', { method: 'POST' }) + return { type: REQUEST_ACTION } +} -export {updateList as updateFlows, fetchList as fetchFlows} +/** + * @public + */ +export function delete(flow) { + fetch(`/flows/${flow.id}`, { method: 'DELETE' }) + return { type: REQUEST_ACTION } +} + +/** + * @public + */ +export function duplicate(flow) { + fetch(`/flows/${flow.id}/duplicate`, { method: 'POST' }) + return { type: REQUEST_ACTION } +} + +/** + * @public + */ +export function replay(flow) { + fetch(`/flows/${flow.id}/replay`, { method: 'POST' }) + return { type: REQUEST_ACTION } +} + +/** + * @public + */ +export function revert(flow) { + fetch(`/flows/${flow.id}/revert`, { method: 'POST' }) + return { type: REQUEST_ACTION } +} + +/** + * @public + */ +export function update(flow, body) { + fetch(`/flows/${flow.id}`, { method: 'PUT', body }) + return { type: REQUEST_ACTION } +} + +/** + * @public + */ +export function clear() { + fetch('/clear', { method: 'POST' }) + return { type: REQUEST_ACTION } +} + +/** + * @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) + fetch('/flows/dump', { method: 'post', body }) + return { type: REQUEST_ACTION } +} + +/** + * @private + */ +export function request() { + return { type: REQUEST } +} + +/** + * @private + */ +export function receive(list) { + return { type: RECEIVE, list } +} + +/** + * @private + */ +export function fetchError(error) { + return { type: FETCH_ERROR, error } +} diff --git a/web/src/js/ducks/utils/list.js b/web/src/js/ducks/utils/list.js index a830fe99..1c1d9692 100644 --- a/web/src/js/ducks/utils/list.js +++ b/web/src/js/ducks/utils/list.js @@ -1,166 +1,150 @@ -import {fetchApi} from "../../utils" - -export const ADD = "ADD" -export const UPDATE = "UPDATE" -export const REMOVE = "REMOVE" -export const REQUEST_LIST = "REQUEST_LIST" -export const RECEIVE_LIST = "RECEIVE_LIST" - - +import * as websocketActions from './websocket' + +export const UPDATE_FILTER = 'LIST_UPDATE_FILTER' +export const UPDATE_SORTER = 'LIST_UPDATE_SORTER' +export const ADD = 'LIST_ADD' +export const UPDATE = 'LIST_UPDATE' +export const REMOVE = 'LIST_REMOVE' +export const UNKNOWN_CMD = 'LIST_UNKNOWN_CMD' +export const REQUEST = 'LIST_REQUEST' +export const RECEIVE = 'LIST_RECEIVE' +export const FETCH_ERROR = 'LIST_FETCH_ERROR' + +export const SYM_FILTER = Symbol('LIST_SYM_FILTER') +export const SYM_SORTER = Symbol('LIST_SYM_SORTER') +export const SYM_PENDING = Symbol('LIST_SYM_PENDING') + +// @todo add indexOf map if necessary const defaultState = { - list: [], - isFetching: false, - actionsDuringFetch: [], + raw: [], + data: [], byId: {}, - indexOf: {}, + isFetching: false, + [SYM_FILTER]: () => true, + [SYM_SORTER]: () => 0, + [SYM_PENDING]: [], } -export default function makeList(actionType, fetchURL) { - function reduceList(state = defaultState, action = {}) { - - if (action.type !== actionType) { - return state +export default function reduce(state = defaultState, action) { + if (state.isFetching && action.type !== RECEIVE) { + return { + ...state, + [SYM_PENDING]: [...state[SYM_PENDING], action] } + } - // Handle cases where we finished fetching or are still fetching. - if (action.cmd === RECEIVE_LIST) { - let s = { - isFetching: false, - actionsDuringFetch: [], - list: action.list, - byId: {}, - indexOf: {} + switch (action.type) { + + case UPDATE_FILTER: + return { + ...state, + [SYM_FILTER]: action.filter, + data: state.raw.filter(action.filter).sort(state[SYM_SORTER]), } - for (let i = 0; i < action.list.length; i++) { - let item = action.list[i] - s.byId[item.id] = item - s.indexOf[item.id] = i + + case UPDATE_SORTER: + return { + ...state, + [SYM_SORTER]: action.sorter, + data: state.data.slice().sort(state[SYM_SORTER]), } - for (action of state.actionsDuringFetch) { - s = reduceList(s, action) + + case ADD: + let data = state.data + if (state[SYM_FILTER](action.item)) { + data = [...state.data, action.item].sort(state[SYM_SORTER]) } - return s - } else if (state.isFetching) { return { ...state, - actionsDuringFetch: [...state.actionsDuringFetch, action] + data, + raw: [...state.raw, action.item], + byId: { ...state.byId, [action.item.id]: action.item }, } - } - let list, itemIndex - switch (action.cmd) { - case ADD: - return { - list: [...state.list, action.item], - byId: {...state.byId, [action.item.id]: action.item}, - indexOf: {...state.indexOf, [action.item.id]: state.list.length}, - } - - case UPDATE: - - list = [...state.list] - itemIndex = state.indexOf[action.item.id] - list[itemIndex] = action.item - return { - ...state, - list, - byId: {...state.byId, [action.item.id]: action.item}, - } - - case REMOVE: - list = [...state.list] - itemIndex = state.indexOf[action.item.id] - list.splice(itemIndex, 1) - return { - ...state, - list, - byId: {...state.byId, [action.item.id]: undefined}, - indexOf: {...state.indexOf, [action.item.id]: undefined}, - } - - case REQUEST_LIST: - return { - ...state, - isFetching: true - } - - default: - console.debug("unknown action", action) - return state - } - } + case UPDATE: + // @todo optimize if necessary + const raw = state.raw.map(item => item.id === action.id ? action.item : item) + return { + ...state, + raw, + data: raw.filter(state[SYM_FILTER]).sort(state[SYM_SORTER]), + byId: { ...state.byId, [action.id]: null, [action.item.id]: action.item }, + } - function addItem(item) { - return { - type: actionType, - cmd: ADD, - item - } - } + case REMOVE: + // @todo optimize if necessary + return { + ...state, + raw: state.raw.filter(item => item.id !== action.id), + data: state.data.filter(item => item.id !== action.id), + byId: { ...state.byId, [action.id]: null }, + } - function updateItem(item) { - return { - type: actionType, - cmd: UPDATE, - item - } - } + case REQUEST: + return { + ...state, + isFetching: true, + } - function removeItem(item) { - return { - type: actionType, - cmd: REMOVE, - item - } + case RECEIVE: + return { + ...state, + isFetching: false, + raw: action.list, + data: action.list.filter(state[SYM_FILTER]).sort(state[SYM_SORTER]), + byId: _.fromPairs(action.list.map(item => [item.id, item])), + } + + default: + return state } +} +export function updateFilter(filter) { + return { type: UPDATE_FILTER, filter } +} - function updateList(event) { - /* This action creater takes all WebSocket events */ - return dispatch => { - switch (event.cmd) { - case "add": - return dispatch(addItem(event.data)) - case "update": - return dispatch(updateItem(event.data)) - case "remove": - return dispatch(removeItem(event.data)) - case "reset": - return dispatch(fetchList()) - default: - console.error("unknown list update", event) - } - } - } +export function updateSorter(sorter) { + return { type: UPDATE_SORTER, sorter } +} - function requestList() { - return { - type: actionType, - cmd: REQUEST_LIST, - } - } +export function add(item) { + return { type: ADD, item } +} - function receiveList(list) { - return { - type: actionType, - cmd: RECEIVE_LIST, - list - } - } +export function update(id, item) { + return { type: UPDATE, id, item } +} - function fetchList() { - return dispatch => { +export function remove(id) { + return { type: REMOVE, id } +} - dispatch(requestList()) +export function handleWsMsg(msg) { + switch (msg.cmd) { - return fetchApi(fetchURL).then(response => { - return response.json().then(json => { - dispatch(receiveList(json.data)) - }) - }) - } + case websocketActions.CMD_ADD: + return add(msg.data) + + case websocketActions.CMD_UPDATE: + return update(msg.data.id, msg.data) + + case websocketActions.CMD_REMOVE: + return remove(msg.data.id) + + default: + return { type: UNKNOWN_CMD, msg } } +} +export function request() { + return { type: REQUEST } +} + +export function receive(list) { + return { type: RECEIVE, list } +} - return {reduceList, updateList, fetchList, addItem, updateItem, removeItem,} -}
\ No newline at end of file +export function fetchError(error) { + return { type: FETCH_ERROR, error } +} diff --git a/web/src/js/ducks/utils/view.js b/web/src/js/ducks/utils/view.js deleted file mode 100644 index 01d57b17..00000000 --- a/web/src/js/ducks/utils/view.js +++ /dev/null @@ -1,134 +0,0 @@ -import {ADD, UPDATE, REMOVE, REQUEST_LIST, RECEIVE_LIST} from "./list" - -const defaultFilterFn = x => true -const defaultSortFn = false - -const makeCompareFn = sortFn => { - let compareFn = (a, b) => { - let akey = sortFn(a), - bkey = sortFn(b) - if (akey < bkey) { - return -1 - } else if (akey > bkey) { - return 1 - } else { - return 0 - } - } - // 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 - if (sortFn && compareFn(list[list.length - 1], item) > 0) { - // TODO: This is untested - console.debug("sorting view...") - l.sort(compareFn) - } - return l -} - -const sortedRemove = (list, sortFn, item) => { - let itemId = item.id - 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) { - sortFn = x => 0 // This triggers the linear search for flows that have the same sort value. - } - - 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(currentView, currentList, nextList, action, filterFn = defaultFilterFn, sortFn = defaultSortFn) { - switch (action.cmd) { - case REQUEST_LIST: - return currentView - case RECEIVE_LIST: - return updateViewFilter(nextList, filterFn, sortFn) - case ADD: - if (filterFn(action.item)) { - return sortedInsert(currentView, sortFn, action.item) - } - 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], - nextItemState = action.item, - isInView = filterFn(currentItemState), - shouldBeInView = filterFn(nextItemState) - - if (!isInView && shouldBeInView) - return sortedInsert(currentView, sortFn, action.item) - if (isInView && !shouldBeInView) - return sortedRemove(currentView, sortFn, action.item) - if (isInView && shouldBeInView) { - let s = [...currentView] - s.indexOf = x => sortedIndexOf(s, x, sortFn) - s[s.indexOf(currentItemState)] = nextItemState - if (sortFn && sortFn(currentItemState) !== sortFn(nextItemState)) - s.sort(makeCompareFn(sortFn)) - return s - } - return currentView - case REMOVE: - let isInView_ = filterFn(currentList.byId[action.item.id]) - if (isInView_) { - return sortedRemove(currentView, sortFn, action.item) - } - return currentView - default: - console.error("Unknown list action: ", action) - return currentView - } -} - -export function updateViewFilter(list, filterFn = defaultFilterFn, sortFn = defaultSortFn) { - let filtered = list.list.filter(filterFn) - if (sortFn){ - filtered.sort(makeCompareFn(sortFn)) - } - filtered.indexOf = x => sortedIndexOf(filtered, x, sortFn) - - return filtered -} - -export function updateViewSort(list, sortFn = defaultSortFn) { - let sorted = [...list] - if (sortFn) { - sorted.sort(makeCompareFn(sortFn)) - } - sorted.indexOf = x => sortedIndexOf(sorted, x, sortFn) - - return sorted -} diff --git a/web/src/js/ducks/websocket.js b/web/src/js/ducks/websocket.js index ebb39cf8..c10f9f5e 100644 --- a/web/src/js/ducks/websocket.js +++ b/web/src/js/ducks/websocket.js @@ -1,6 +1,10 @@ const CONNECTED = 'WEBSOCKET_CONNECTED' const DISCONNECTED = 'WEBSOCKET_DISCONNECTED' +export const CMD_ADD = 'add' +export const CMD_UPDATE = 'update' +export const CMD_REMOVE = 'remove' +export const CMD_RESET = 'reset' const defaultState = { connected: false, |