aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/js/ducks
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/js/ducks')
-rw-r--r--web/src/js/ducks/eventLog.js178
-rw-r--r--web/src/js/ducks/flows.js274
-rw-r--r--web/src/js/ducks/utils/list.js209
-rwxr-xr-x[-rw-r--r--]web/src/js/ducks/utils/view.js243
-rwxr-xr-xweb/src/js/ducks/views.js40
-rwxr-xr-xweb/src/js/ducks/views/main.js199
-rw-r--r--web/src/js/ducks/websocket.js15
7 files changed, 769 insertions, 389 deletions
diff --git a/web/src/js/ducks/eventLog.js b/web/src/js/ducks/eventLog.js
index 44b67b2c..0c875689 100644
--- a/web/src/js/ducks/eventLog.js
+++ b/web/src/js/ducks/eventLog.js
@@ -1,80 +1,152 @@
-import makeList from "./utils/list"
-import {updateViewFilter, updateViewList} from "./utils/view"
+import { fetchApi } from '../utils'
+import reduceList, * as listActions from './utils/list'
+import reduceView, * as viewActions from './utils/view'
+import * as websocketActions from './websocket'
-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_EVENTLOG'
+export const ADD = 'EVENTLOG_ADD'
+export const REQUEST = 'EVENTLOG_REQUEST'
+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'
const defaultState = {
+ logId: 0,
visible: false,
- filter: {
- "debug": false,
- "info": true,
- "web": true
- },
- events: reduceList(),
- filteredEvents: [],
+ filters: { debug: false, info: true, web: true },
+ list: undefined,
+ view: undefined,
}
-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,
+ view: reduceView(state.view, viewActions.updateFilter(state.list, log => filters[log.level])),
+ }
+
+ case ADD:
+ const item = {
+ id: `log-${state.logId}`,
+ message: action.message,
+ level: action.level,
}
return {
...state,
- filter,
- filteredEvents: updateViewFilter(
- state.events,
- x => filter[x.level]
- )
+ logId: state.logId + 1,
+ list: reduceList(state.list, listActions.add(item)),
+ view: reduceView(state.view, viewActions.add(item, log => state.filters[log.level])),
}
- case TOGGLE_VISIBILITY:
+
+ case REQUEST:
return {
...state,
- visible: !state.visible
+ list: reduceList(state.list, listActions.request()),
+ }
+
+ case RECEIVE:
+ const list = reduceList(state.list, listActions.receive(action.list))
+ return {
+ ...state,
+ list,
+ view: reduceView(state.view, viewActions.receive(list, log => state.filters[log.level])),
}
- case UPDATE_LOG:
- const events = reduceList(state.events, action)
+
+ default:
return {
...state,
- events,
- filteredEvents: updateViewList(
- state.filteredEvents,
- state.events,
- events,
- action,
- x => state.filter[x.level]
- )
+ list: reduceList(state.list, action),
+ view: reduceView(state.view, action),
}
+ }
+}
+
+/**
+ * @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) {
+ switch (msg.cmd) {
+
+ case websocketActions.CMD_ADD:
+ return add(msg.data.message, msg.data.level)
+
+ case websocketActions.CMD_RESET:
+ return fetchData()
+
default:
- return state
+ return { type: UNKNOWN_CMD, msg }
}
}
+/**
+ * @public websocket
+ */
+export function fetchData() {
+ return dispatch => {
+ dispatch(request())
-export function toggleEventLogFilter(filter) {
- return {type: TOGGLE_FILTER, filter}
+ return fetchApi('/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 }
+}
+
+/**
+ * @private
+ */
+export function receive(list) {
+ return { type: RECEIVE, list }
}
-let id = 0
-export function addLogEntry(message, level = "web") {
- return addItem({
- message,
- level,
- id: `log-${id++}`
- })
+
+/**
+ * @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..f732f536 100644
--- a/web/src/js/ducks/flows.js
+++ b/web/src/js/ducks/flows.js
@@ -1,114 +1,230 @@
-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 } from '../utils'
+import reduceList, * as listActions from './utils/list'
+import reduceViews, * as viewsActions from './views'
+import * as websocketActions from './websocket'
-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 ADD = 'FLOWS_ADD'
+export const UPDATE = 'FLOWS_UPDATE'
+export const REMOVE = 'FLOWS_REMOVE'
+export const REQUEST = 'FLOWS_REQUEST'
+export const RECEIVE = 'FLOWS_RECEIVE'
+export const REQUEST_ACTION = 'FLOWS_REQUEST_ACTION'
+export const UNKNOWN_CMD = 'FLOWS_UNKNOWN_CMD'
+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;
-}
-
-
-function makeSortFn(sort){
- let column = columns[sort.sortColumn];
- if (!column) return;
-
- let sortKeyFun = column.sortKeyFun;
- if (sort.sortDesc) {
- sortKeyFun = sortKeyFun && function (flow) {
- const k = column.sortKeyFun(flow);
- return _.isString(k) ? reverseString("" + k) : -k;
- };
- }
- return sortKeyFun;
+ list: undefined,
+ views: undefined,
}
-export default function reducer(state = defaultState, action) {
+export default function reduce(state = defaultState, action) {
switch (action.type) {
- case UPDATE_FLOWS:
- let all = reduceList(state.all, action)
+
+ case ADD:
return {
...state,
- all,
- view: updateViewList(state.view, state.all, all, action, makeFilterFn(action.filter), makeSortFn(state.sort))
+ list: reduceList(state.list, listActions.add(action.item)),
+ views: reduceViews(state.views, viewsActions.add(action.item)),
}
- case SET_FILTER:
+
+ case UPDATE:
return {
...state,
- filter: action.filter,
- view: updateViewFilter(state.all, makeFilterFn(action.filter), makeSortFn(state.sort))
+ list: reduceList(state.list, listActions.update(action.id, action.item)),
+ views: reduceViews(state.views, viewsActions.update(action.id, action.item)),
}
- case SET_HIGHLIGHT:
+
+ case REMOVE:
return {
...state,
- highlight: action.highlight
+ list: reduceList(state.list, listActions.remove(action.item.id)),
+ views: reduceViews(state.views, viewsActions.remove(action.item.id)),
}
- 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:
+ const list = reduceList(state.list, listActions.receive(action.list))
return {
...state,
- selected: [action.flowId]
+ list,
+ views: reduceViews(state.views, viewsActions.receive(list)),
}
+
default:
- return state
+ return {
+ ...state,
+ list: reduceList(state.list, action),
+ views: reduceViews(state.views, action),
+ }
}
}
+/**
+ * @public
+ */
+export function accept(flow) {
+ fetchApi(`/flows/${flow.id}/accept`, { method: 'POST' })
+ return { type: REQUEST_ACTION }
+}
-export function setFilter(filter) {
- return {
- type: SET_FILTER,
- filter
- }
+/**
+ * @public
+ */
+export function acceptAll() {
+ fetchApi('/flows/accept', { method: 'POST' })
+ return { type: REQUEST_ACTION }
}
-export function setHighlight(highlight) {
- return {
- type: SET_HIGHLIGHT,
- highlight
- }
+
+/**
+ * @public
+ */
+export function remove(flow) {
+ fetchApi(`/flows/${flow.id}`, { method: 'DELETE' })
+ return { type: REQUEST_ACTION }
+}
+
+/**
+ * @public
+ */
+export function duplicate(flow) {
+ fetchApi(`/flows/${flow.id}/duplicate`, { method: 'POST' })
+ return { type: REQUEST_ACTION }
+}
+
+/**
+ * @public
+ */
+export function replay(flow) {
+ fetchApi(`/flows/${flow.id}/replay`, { method: 'POST' })
+ return { type: REQUEST_ACTION }
+}
+
+/**
+ * @public
+ */
+export function revert(flow) {
+ fetchApi(`/flows/${flow.id}/revert`, { method: 'POST' })
+ return { type: REQUEST_ACTION }
}
-export function setSort(sort){
- return {
- type: SET_SORT,
- sort
+
+/**
+ * @public
+ */
+export function update(flow, body) {
+ fetchApi(`/flows/${flow.id}`, { method: 'PUT', body })
+ return { type: REQUEST_ACTION }
+}
+
+/**
+ * @public
+ */
+export function clear() {
+ fetchApi('/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)
+ fetchApi('/flows/dump', { method: 'post', body })
+ return { type: REQUEST_ACTION }
+}
+
+/**
+ * This action creater takes all WebSocket events
+ *
+ * @public websocket
+ */
+export function handleWsMsg(msg) {
+ switch (msg.cmd) {
+
+ 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)
+
+ case websocketActions.CMD_RESET:
+ return fetchData()
+
+ default:
+ return { type: UNKNOWN_CMD, 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 fetchApi('/flows')
+ .then(res => res.json())
+ .then(json => dispatch(receive(json.data)))
+ .catch(error => dispatch(fetchError(error)))
}
}
+/**
+ * @private
+ */
+export function add(item) {
+ return { type: ADD, item }
+}
-export {updateList as updateFlows, fetchList as fetchFlows}
+/**
+ * @private
+ */
+export function update(id, item) {
+ return { type: UPDATE, id, item }
+}
+
+/**
+ * @private
+ */
+export function remove(id) {
+ return { type: REMOVE, id }
+}
+
+/**
+ * @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..e66a8549 100644
--- a/web/src/js/ducks/utils/list.js
+++ b/web/src/js/ducks/utils/list.js
@@ -1,166 +1,91 @@
-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 _ from 'lodash'
+export const SET = 'LIST_SET'
+export const CLEAR = 'LIST_CLEAR'
+export const REQUEST = 'LIST_REQUEST'
+export const RECEIVE = 'LIST_RECEIVE'
const defaultState = {
- list: [],
- isFetching: false,
- actionsDuringFetch: [],
- byId: {},
- indexOf: {},
+ data: {},
+ pendingActions: null,
}
-export default function makeList(actionType, fetchURL) {
- function reduceList(state = defaultState, action = {}) {
-
- if (action.type !== actionType) {
- return state
- }
+export default function reduce(state = defaultState, action) {
+ switch (action.type) {
- // 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: {}
- }
- for (let i = 0; i < action.list.length; i++) {
- let item = action.list[i]
- s.byId[item.id] = item
- s.indexOf[item.id] = i
- }
- for (action of state.actionsDuringFetch) {
- s = reduceList(s, action)
+ case SET:
+ if (state.pendingActions) {
+ return {
+ ...state,
+ pendingActions: [...state.pendingActions, action]
+ }
}
- return s
- } else if (state.isFetching) {
return {
...state,
- actionsDuringFetch: [...state.actionsDuringFetch, action]
+ data: { ...state.data, [action.id]: null, [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
+ case CLEAR:
+ if (state.pendingActions) {
return {
...state,
- list,
- byId: {...state.byId, [action.item.id]: action.item},
+ pendingActions: [...state.pendingActions, action]
}
+ }
+ return {
+ ...state,
+ data: { ...state.data, [action.id]: null }
+ }
- 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
- }
- }
-
- function addItem(item) {
- return {
- type: actionType,
- cmd: ADD,
- item
- }
- }
-
- function updateItem(item) {
- return {
- type: actionType,
- cmd: UPDATE,
- item
- }
- }
-
- function removeItem(item) {
- return {
- type: actionType,
- cmd: REMOVE,
- item
- }
- }
-
-
- 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)
+ case REQUEST:
+ return {
+ ...state,
+ pendingActions: []
}
- }
- }
- function requestList() {
- return {
- type: actionType,
- cmd: REQUEST_LIST,
- }
- }
+ case RECEIVE:
+ return state.pendingActions.reduce(reduce, {
+ ...state,
+ pendingActions: null,
+ data: _.fromPairs(action.list.map(item => [item.id, item])),
+ })
- function receiveList(list) {
- return {
- type: actionType,
- cmd: RECEIVE_LIST,
- list
- }
+ default:
+ return state
}
+}
- function fetchList() {
- return dispatch => {
+/**
+ * @public
+ */
+export function add(item) {
+ return { type: SET, id: item.id, item }
+}
- dispatch(requestList())
+/**
+ * @public
+ */
+export function update(id, item) {
+ return { type: SET, id, item }
+}
- return fetchApi(fetchURL).then(response => {
- return response.json().then(json => {
- dispatch(receiveList(json.data))
- })
- })
- }
- }
+/**
+ * @public
+ */
+export function remove(id) {
+ return { type: CLEAR, id }
+}
+/**
+ * @public
+ */
+export function request() {
+ return { type: REQUEST }
+}
- return {reduceList, updateList, fetchList, addItem, updateItem, removeItem,}
-} \ No newline at end of file
+/**
+ * @public
+ */
+export function receive(list) {
+ return { type: RECEIVE, list }
+}
diff --git a/web/src/js/ducks/utils/view.js b/web/src/js/ducks/utils/view.js
index 01d57b17..3b552378 100644..100755
--- a/web/src/js/ducks/utils/view.js
+++ b/web/src/js/ducks/utils/view.js
@@ -1,134 +1,157 @@
-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
-}
+import _ from 'lodash'
-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
-}
+export const UPDATE_FILTER = 'VIEW_UPDATE_FILTER'
+export const UPDATE_SORTER = 'VIEW_UPDATE_SORTER'
+export const ADD = 'VIEW_ADD'
+export const UPDATE = 'VIEW_UPDATE'
+export const REMOVE = 'VIEW_REMOVE'
+export const RECEIVE = 'VIEW_RECEIVE'
-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
+const defaultState = {
+ data: [],
+ indexOf: {},
}
-export function sortedIndexOf(list, value, sortFn) {
- if (!sortFn) {
- sortFn = x => 0 // This triggers the linear search for flows that have the same sort value.
- }
+export default function reduce(state = defaultState, action) {
+ switch (action.type) {
- 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
+ case UPDATE_FILTER: {
+ const data = _.values(action.list.data).filter(action.filter).sort(action.sorter)
+ return {
+ ...state,
+ data,
+ indexOf: _.fromPairs(data.map((item, index) => [item.id, index])),
+ }
}
- }
- // 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;
-}
+ case UPDATE_SORTER: {
+ const data = [...state.data].sort(action.sorter)
+ return {
+ ...state,
+ data,
+ indexOf: _.fromPairs(data.map((item, index) => [item.id, index]))
+ }
+ }
-// 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)
+ if (state.indexOf[action.item.id] != null || !action.filter(action.item)) {
+ return state
}
- return currentView
- case UPDATE:
- // let's determine if it's in the view currently and if it should be in the view.
- let currentItemState = currentList.byId[action.item.id],
- 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 {
+ ...state,
+ ...sortedInsert(state, action.item, action.sorter),
}
- return currentView
+
case REMOVE:
- let isInView_ = filterFn(currentList.byId[action.item.id])
- if (isInView_) {
- return sortedRemove(currentView, sortFn, action.item)
+ if (state.indexOf[action.item.id] == null) {
+ return state
+ }
+ return {
+ ...state,
+ ...sortedRemove(state, action.id),
+ }
+
+ case UPDATE: {
+ if (state.indexOf[action.item.id] == null) {
+ return
+ }
+ const nextState = {
+ ...state,
+ ...sortedRemove(state, action.id),
}
- return currentView
+ if (!action.filter(action.item)) {
+ return nextState
+ }
+ return {
+ ...nextState,
+ ...sortedInsert(nextState, action.item, action.sorter)
+ }
+ }
+
+ case RECEIVE: {
+ const data = _.values(action.list.data).filter(action.filter).sort(action.sorter)
+ return {
+ ...state,
+ data,
+ indexOf: _.fromPairs(data.map((item, index) => [item.id, index])),
+ }
+ }
+
default:
- console.error("Unknown list action: ", action)
- return currentView
+ return state
}
}
-export function updateViewFilter(list, filterFn = defaultFilterFn, sortFn = defaultSortFn) {
- let filtered = list.list.filter(filterFn)
- if (sortFn){
- filtered.sort(makeCompareFn(sortFn))
+export function updateFilter(list, filter = defaultFilter, sorter = defaultSorter) {
+ return { type: UPDATE_FILTER, list, filter, sorter }
+}
+
+export function updateSorter(sorter = defaultSorter) {
+ return { type: UPDATE_SORTER, sorter }
+}
+
+export function add(item, filter = defaultFilter, sorter = defaultSorter) {
+ return { type: ADD, item, filter, sorter }
+}
+
+export function update(id, item, filter = defaultFilter, sorter = defaultSorter) {
+ return { type: UPDATE, id, item, filter, sorter }
+}
+
+export function remove(id) {
+ return { type: REMOVE, id }
+}
+
+export function receive(list, filter = defaultFilter, sorter = defaultSorter) {
+ return { type: RECEIVE, list, filter, sorter }
+}
+
+function sortedInsert(state, item, sorter) {
+ const index = sortedIndex(state.data, item, sorter)
+ 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
}
- filtered.indexOf = x => sortedIndexOf(filtered, x, sortFn)
- return filtered
+ return { data, indexOf }
}
-export function updateViewSort(list, sortFn = defaultSortFn) {
- let sorted = [...list]
- if (sortFn) {
- sorted.sort(makeCompareFn(sortFn))
+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
}
- sorted.indexOf = x => sortedIndexOf(sorted, x, sortFn)
- return sorted
+ return { data, indexOf }
+}
+
+function sortedIndex(list, item, sorter) {
+ let low = 0
+ let high = list.length
+
+ while (low < high) {
+ const middle = (low + high) >>> 1
+ if (sorter(item, list[middle]) > 0) {
+ low = middle + 1
+ } else {
+ high = middle
+ }
+ }
+
+ return low
+}
+
+function defaultFilter() {
+ return true
+}
+
+function defaultSorter(a, b) {
+ return 0
}
diff --git a/web/src/js/ducks/views.js b/web/src/js/ducks/views.js
new file mode 100755
index 00000000..e1e46c2e
--- /dev/null
+++ b/web/src/js/ducks/views.js
@@ -0,0 +1,40 @@
+import { combineReducers } from 'redux'
+import * as viewActions from './utils/view'
+import main from './views/main.js'
+
+export const ADD = 'FLOW_VIEWS_ADD'
+export const UPDATE = 'FLOW_VIEWS_UPDATE'
+export const REMOVE = 'FLOW_VIEWS_REMOVE'
+export const RECEIVE = 'FLOW_VIEWS_RECEIVE'
+
+export default combineReducers({
+ main,
+})
+
+/**
+ * @public
+ */
+export function add(item) {
+ return { type: ADD, item }
+}
+
+/**
+ * @public
+ */
+export function update(id, item) {
+ return { type: UPDATE, id, 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/views/main.js b/web/src/js/ducks/views/main.js
new file mode 100755
index 00000000..74dc2606
--- /dev/null
+++ b/web/src/js/ducks/views/main.js
@@ -0,0 +1,199 @@
+import { RequestUtils } from '../../flow/utils'
+import reduceView, * as viewActions from '../utils/view'
+import * as viewsActions from '../views'
+
+export const UPDATE_FILTER = 'FLOW_VIEWS_MAIN_UPDATE_FILTER'
+export const UPDATE_SORTER = 'FLOW_VIEWS_MAIN_UPDATE_SORTER'
+export const UPDATE_HIGHLIGHT = 'FLOW_VIEWS_MAIN_UPDATE_HIGHLIGHT'
+export const SELECT = 'FLOW_VIEWS_MAIN_SELECT'
+
+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
+ },
+}
+
+const defaultState = {
+ highlight: null,
+ selected: [],
+ filter: null,
+ sorter: { column: null, desc: false },
+ view: undefined,
+}
+
+export default function reduce(state = defaultState, action) {
+ switch (action.type) {
+
+ case UPDATE_HIGHLIGHT:
+ return {
+ ...state,
+ highlight: action.highlight,
+ }
+
+ case SELECT:
+ return {
+ ...state,
+ selected: [action.id]
+ }
+
+ case UPDATE_FILTER:
+ return {
+ ...state,
+ filter: action.filter,
+ view: reduceView(
+ state.view,
+ viewActions.updateFilter(
+ action.list,
+ makeFilter(action.filter),
+ makeSorter(state.sorter)
+ )
+ ),
+ }
+
+ case UPDATE_SORTER:
+ const sorter = { column: action.column, desc: action.desc }
+ return {
+ ...state,
+ sorter,
+ view: reduceView(
+ state.view,
+ viewActions.updateSorter(
+ makeSorter(sorter)
+ )
+ ),
+ }
+
+ case viewsActions.ADD:
+ return {
+ ...state,
+ view: reduceView(
+ state.view,
+ viewActions.add(
+ action.item,
+ makeFilter(state.filter),
+ makeSorter(state.sorter)
+ )
+ ),
+ }
+
+ case viewsActions.UPDATE:
+ return {
+ ...state,
+ view: reduceView(
+ state.view,
+ viewActions.update(
+ action.id,
+ action.item,
+ makeFilter(state.filter),
+ makeSorter(state.sorter)
+ )
+ ),
+ }
+
+ case viewsActions.REMOVE:
+ return {
+ ...state,
+ view: reduceView(
+ state.view,
+ viewActions.remove(
+ action.id
+ )
+ ),
+ }
+
+ case viewsActions.RECEIVE:
+ return {
+ ...state,
+ view: reduceView(
+ state.view,
+ viewActions.receive(
+ action.list,
+ makeFilter(state.filter),
+ makeSorter(state.sorter)
+ )
+ ),
+ }
+
+ default:
+ return {
+ ...state,
+ view: reduceView(state.view, action)
+ }
+ }
+}
+
+/**
+ * @public
+ */
+export function updateFilter(filter) {
+ return (dispatch, getState) => {
+ return { type: UPDATE_FILTER, filter, list: getState().flows.list }
+ }
+}
+
+/**
+ * @public
+ */
+export function updateHighlight(highlight) {
+ return { type: UPDATE_HIGHLIGHT, highlight }
+}
+
+/**
+ * @public
+ */
+export function updateSorter(column, desc) {
+ return { type: UPDATE_SORTER, column, desc }
+}
+
+/**
+ * @public
+ */
+export function select(id) {
+ return { type: SELECT, currentSelection: getState().flows.views.main.selected[0], id }
+}
+
+/**
+ * @private
+ */
+function makeFilter(filter) {
+ if (!filter) {
+ return
+ }
+ return Filt.parse(filter)
+}
+
+/**
+ * @private
+ */
+function makeSorter({ 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
+ }
+}
diff --git a/web/src/js/ducks/websocket.js b/web/src/js/ducks/websocket.js
index f38124c9..c79d887a 100644
--- a/web/src/js/ducks/websocket.js
+++ b/web/src/js/ducks/websocket.js
@@ -4,6 +4,11 @@ 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'
@@ -78,14 +83,14 @@ export function onMessage(msg) {
switch (data.type) {
- case eventLogActions.UPDATE_LOG:
- return dispatch(eventLogActions.updateLogEntries(data))
+ case eventLogActions.WS_MSG_TYPE:
+ return dispatch(eventLogActions.handleWsMsg(data))
- case flowsActions.UPDATE_FLOWS:
- return dispatch(flowsActions.updateFlows(data))
+ case flowsActions.WS_MSG_TYPE:
+ return dispatch(flowsActions.handleWsMsg(data))
case settingsActions.UPDATE_SETTINGS:
- return dispatch(settingsActions.updateSettings(message))
+ return dispatch(settingsActions.handleWsMsg(data))
default:
console.warn('unknown message', data)