aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--web/src/js/__tests__/ducks/ui.js4
-rw-r--r--web/src/js/actions.js55
-rw-r--r--web/src/js/app.jsx4
-rw-r--r--web/src/js/components/EventLog.jsx6
-rw-r--r--web/src/js/components/FlowTable/FlowTableHead.jsx6
-rw-r--r--web/src/js/components/Header.jsx1
-rw-r--r--web/src/js/components/Header/ViewMenu.jsx4
-rw-r--r--web/src/js/components/MainView.jsx10
-rw-r--r--web/src/js/connection.js16
-rw-r--r--web/src/js/ducks/eventLog.js162
-rw-r--r--web/src/js/ducks/flows.js282
-rw-r--r--web/src/js/ducks/utils/list.js262
-rw-r--r--web/src/js/ducks/utils/view.js134
-rw-r--r--web/src/js/ducks/websocket.js4
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,