web: simplify flow storage

This commit is contained in:
Maximilian Hils 2016-11-01 00:04:05 +01:00
parent 85476d9915
commit c2a130dced
17 changed files with 365 additions and 575 deletions

View File

@ -251,7 +251,7 @@ class FlowHandler(RequestHandler):
elif k == "port": elif k == "port":
request.port = int(v) request.port = int(v)
elif k == "headers": elif k == "headers":
request.headers.set_state(v) request.headers.set_state((a.encode(), b.encode()) for a, b in v)
elif k == "content": elif k == "content":
request.text = v request.text = v
else: else:

View File

@ -21,7 +21,8 @@ class Stop(Exception):
class _WebState(): class _WebState():
def add_log(self, e, level): def add_log(self, e, level):
self._last_event_id += 1 # server-side log ids are odd
self._last_event_id += 2
entry = { entry = {
"id": self._last_event_id, "id": self._last_event_id,
"message": e, "message": e,

View File

@ -48,9 +48,9 @@ export default class WebsocketBackend {
} }
} }
receive(resource, msg) { receive(resource, data) {
let type = `${resource}_RECEIVE`.toUpperCase() let type = `${resource}_RECEIVE`.toUpperCase()
this.store.dispatch({ type, [resource]: msg }) this.store.dispatch({ type, cmd: "receive", resource, data })
let queue = this.activeFetches[resource] let queue = this.activeFetches[resource]
delete this.activeFetches[resource] delete this.activeFetches[resource]
queue.forEach(msg => this.onMessage(msg)) queue.forEach(msg => this.onMessage(msg))

View File

@ -3,15 +3,15 @@ import { connect } from 'react-redux'
import classnames from 'classnames' import classnames from 'classnames'
import columns from './FlowColumns' import columns from './FlowColumns'
import { updateSort } from '../../ducks/flowView' import { setSort } from '../../ducks/flows'
FlowTableHead.propTypes = { FlowTableHead.propTypes = {
updateSort: PropTypes.func.isRequired, setSort: PropTypes.func.isRequired,
sortDesc: React.PropTypes.bool.isRequired, sortDesc: React.PropTypes.bool.isRequired,
sortColumn: React.PropTypes.string, sortColumn: React.PropTypes.string,
} }
function FlowTableHead({ sortColumn, sortDesc, updateSort }) { function FlowTableHead({ sortColumn, sortDesc, setSort }) {
const sortType = sortDesc ? 'sort-desc' : 'sort-asc' const sortType = sortDesc ? 'sort-desc' : 'sort-asc'
return ( return (
@ -19,7 +19,7 @@ function FlowTableHead({ sortColumn, sortDesc, updateSort }) {
{columns.map(Column => ( {columns.map(Column => (
<th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)} <th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)}
key={Column.name} key={Column.name}
onClick={() => updateSort(Column.name, Column.name !== sortColumn ? false : !sortDesc)}> onClick={() => setSort(Column.name, Column.name !== sortColumn ? false : !sortDesc)}>
{Column.headerName} {Column.headerName}
</th> </th>
))} ))}
@ -29,10 +29,10 @@ function FlowTableHead({ sortColumn, sortDesc, updateSort }) {
export default connect( export default connect(
state => ({ state => ({
sortDesc: state.flowView.sort.desc, sortDesc: state.flows.sort.desc,
sortColumn: state.flowView.sort.column, sortColumn: state.flows.sort.column,
}), }),
{ {
updateSort setSort
} }
)(FlowTableHead) )(FlowTableHead)

View File

@ -2,7 +2,7 @@ import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import FilterInput from './FilterInput' import FilterInput from './FilterInput'
import { update as updateSettings } from '../../ducks/settings' import { update as updateSettings } from '../../ducks/settings'
import { updateFilter, updateHighlight } from '../../ducks/flowView' import { setFilter, setHighlight } from '../../ducks/flows'
MainMenu.title = "Start" MainMenu.title = "Start"
@ -31,20 +31,20 @@ const InterceptInput = connect(
const FlowFilterInput = connect( const FlowFilterInput = connect(
state => ({ state => ({
value: state.flowView.filter || '', value: state.flows.filter || '',
placeholder: 'Search', placeholder: 'Search',
type: 'search', type: 'search',
color: 'black' color: 'black'
}), }),
{ onChange: updateFilter } { onChange: setFilter }
)(FilterInput); )(FilterInput);
const HighlightInput = connect( const HighlightInput = connect(
state => ({ state => ({
value: state.flowView.highlight || '', value: state.flows.highlight || '',
placeholder: 'Highlight', placeholder: 'Highlight',
type: 'tag', type: 'tag',
color: 'hsl(48, 100%, 50%)' color: 'hsl(48, 100%, 50%)'
}), }),
{ onChange: updateHighlight } { onChange: setHighlight }
)(FilterInput); )(FilterInput);

View File

@ -4,7 +4,6 @@ import Splitter from './common/Splitter'
import FlowTable from './FlowTable' import FlowTable from './FlowTable'
import FlowView from './FlowView' import FlowView from './FlowView'
import * as flowsActions from '../ducks/flows' import * as flowsActions from '../ducks/flows'
import { updateFilter, updateHighlight } from '../ducks/flowView'
class MainView extends Component { class MainView extends Component {
@ -41,16 +40,14 @@ class MainView extends Component {
export default connect( export default connect(
state => ({ state => ({
flows: state.flowView.data, flows: state.flows.view,
filter: state.flowView.filter, filter: state.flows.filter,
highlight: state.flowView.highlight, highlight: state.flows.highlight,
selectedFlow: state.flows.byId[state.flows.selected[0]], selectedFlow: state.flows.byId[state.flows.selected[0]],
tab: state.ui.flow.tab, tab: state.ui.flow.tab,
}), }),
{ {
selectFlow: flowsActions.select, selectFlow: flowsActions.select,
updateFilter,
updateHighlight,
updateFlow: flowsActions.update, updateFlow: flowsActions.update,
} }
)(MainView) )(MainView)

View File

@ -1,19 +1,15 @@
import reduceList, * as listActions from './utils/list' import reduceStore from "./utils/store"
import reduceView, * as viewActions from './utils/view' import * as storeActions from "./utils/store"
export const ADD = 'EVENTS_ADD' export const ADD = 'EVENTS_ADD'
export const RECEIVE = 'EVENTS_RECEIVE' export const RECEIVE = 'EVENTS_RECEIVE'
export const TOGGLE_VISIBILITY = 'EVENTS_TOGGLE_VISIBILITY' export const TOGGLE_VISIBILITY = 'EVENTS_TOGGLE_VISIBILITY'
export const TOGGLE_FILTER = 'EVENTS_TOGGLE_FILTER' export const TOGGLE_FILTER = 'EVENTS_TOGGLE_FILTER'
export const UNKNOWN_CMD = 'EVENTS_UNKNOWN_CMD'
export const FETCH_ERROR = 'EVENTS_FETCH_ERROR'
const defaultState = { const defaultState = {
logId: 0,
visible: false, visible: false,
filters: { debug: false, info: true, web: true }, filters: { debug: false, info: true, web: true },
list: reduceList(undefined, {}), ...reduceStore(undefined, {}),
view: reduceView(undefined, {}),
} }
export default function reduce(state = defaultState, action) { export default function reduce(state = defaultState, action) {
@ -30,27 +26,14 @@ export default function reduce(state = defaultState, action) {
return { return {
...state, ...state,
filters, filters,
view: reduceView(state.view, viewActions.updateFilter(state.list.data, log => filters[log.level])), ...reduceStore(state, storeActions.setFilter(log => filters[log.level]))
} }
case ADD: case ADD:
const item = {
id: state.logId,
message: action.message,
level: action.level,
}
return {
...state,
logId: state.logId + 1,
list: reduceList(state.list, listActions.add(item)),
view: reduceView(state.view, viewActions.add(item, log => state.filters[log.level])),
}
case RECEIVE: case RECEIVE:
return { return {
...state, ...state,
list: reduceList(state.list, listActions.receive(action.events)), ...reduceStore(state, storeActions[action.cmd](action.data, log => state.filters[log.level]))
view: reduceView(state.view, viewActions.receive(action.events, log => state.filters[log.level])),
} }
default: default:
@ -66,6 +49,17 @@ export function toggleVisibility() {
return { type: TOGGLE_VISIBILITY } return { type: TOGGLE_VISIBILITY }
} }
let logId = 1 // client-side log ids are odd
export function add(message, level = 'web') { export function add(message, level = 'web') {
return { type: ADD, message, level } let data = {
id: logId,
message,
level,
}
logId += 2
return {
type: ADD,
cmd: "add",
data
}
} }

View File

@ -1,192 +0,0 @@
import reduceView, * as viewActions from './utils/view'
import * as flowActions from './flows'
import Filt from '../filt/filt'
import { RequestUtils } from '../flow/utils'
export const UPDATE_FILTER = 'FLOWVIEW_UPDATE_FILTER'
export const UPDATE_SORT = 'FLOWVIEW_UPDATE_SORT'
export const UPDATE_HIGHLIGHT = 'FLOWVIEW_UPDATE_HIGHLIGHT'
const sortKeyFuns = {
TLSColumn: flow => flow.request.scheme,
PathColumn: flow => RequestUtils.pretty_url(flow.request),
MethodColumn: flow => flow.request.method,
StatusColumn: flow => flow.response && flow.response.status_code,
TimeColumn: flow => flow.response && flow.response.timestamp_end - flow.request.timestamp_start,
SizeColumn: flow => {
let total = flow.request.contentLength
if (flow.response) {
total += flow.response.contentLength || 0
}
return total
},
}
export function makeFilter(filter) {
if (!filter) {
return
}
return Filt.parse(filter)
}
export function makeSort({ column, desc }) {
const sortKeyFun = sortKeyFuns[column]
if (!sortKeyFun) {
return
}
return (a, b) => {
const ka = sortKeyFun(a)
const kb = sortKeyFun(b)
if (ka > kb) {
return desc ? -1 : 1
}
if (ka < kb) {
return desc ? 1 : -1
}
return 0
}
}
const defaultState = {
highlight: null,
filter: null,
sort: { column: null, desc: false },
...reduceView(undefined, {})
}
export default function reduce(state = defaultState, action) {
switch (action.type) {
case UPDATE_HIGHLIGHT:
return {
...state,
highlight: action.highlight,
}
case UPDATE_FILTER:
return {
...reduceView(
state,
viewActions.updateFilter(
action.flows,
makeFilter(action.filter),
makeSort(state.sort)
)
),
filter: action.filter,
}
case UPDATE_SORT:
const sort = { column: action.column, desc: action.desc }
return {
...reduceView(
state,
viewActions.updateSort(
makeSort(sort)
)
),
sort,
}
case flowActions.ADD:
return {
...reduceView(
state,
viewActions.add(
action.item,
makeFilter(state.filter),
makeSort(state.sort)
)
),
}
case flowActions.UPDATE:
return {
...reduceView(
state,
viewActions.update(
action.item,
makeFilter(state.filter),
makeSort(state.sort)
)
),
}
case flowActions.REMOVE:
/* FIXME: Implement select switch on remove
return (dispatch, getState) => {
let currentIndex = getState().flowView.indexOf[getState().flows.selected[0]]
let maxIndex = getState().flowView.data.length - 1
let deleteLastEntry = maxIndex == 0
if (deleteLastEntry)
dispatch(select())
else
dispatch(selectRelative(currentIndex == maxIndex ? -1 : 1) )
*/
return {
...reduceView(
state,
viewActions.remove(
action.id
)
),
}
case flowActions.RECEIVE:
return {
...reduceView(
state,
viewActions.receive(
action.flows,
makeFilter(state.filter),
makeSort(state.sort)
)
),
}
default:
return {
...reduceView(state, action),
}
}
}
export function updateFilter(filter) {
return (dispatch, getState) => {
dispatch({ type: UPDATE_FILTER, filter, flows: getState().flows.data })
}
}
export function updateHighlight(highlight) {
return { type: UPDATE_HIGHLIGHT, highlight }
}
export function updateSort(column, desc) {
return { type: UPDATE_SORT, column, desc }
}
export function selectRelative(shift) {
return (dispatch, getState) => {
let currentSelectionIndex = getState().flowView.indexOf[getState().flows.selected[0]]
let minIndex = 0
let maxIndex = getState().flowView.data.length - 1
let newIndex
if (currentSelectionIndex === undefined) {
newIndex = (shift < 0) ? minIndex : maxIndex
} else {
newIndex = currentSelectionIndex + shift
newIndex = Math.max(newIndex, minIndex)
newIndex = Math.min(newIndex, maxIndex)
}
let flow = getState().flowView.data[newIndex]
dispatch(flowActions.select(flow ? flow.id : undefined))
}
}

View File

@ -1,47 +1,58 @@
import { fetchApi } from '../utils' import { fetchApi } from "../utils"
import reduceList, * as listActions from './utils/list' import reduceStore from "./utils/store"
import { selectRelative } from './flowView' import * as storeActions from "./utils/store"
import Filt from "../filt/filt"
import { RequestUtils } from "../flow/utils"
export const ADD = 'FLOWS_ADD' export const ADD = 'FLOWS_ADD'
export const UPDATE = 'FLOWS_UPDATE' export const UPDATE = 'FLOWS_UPDATE'
export const REMOVE = 'FLOWS_REMOVE' export const REMOVE = 'FLOWS_REMOVE'
export const RECEIVE = 'FLOWS_RECEIVE' export const RECEIVE = 'FLOWS_RECEIVE'
export const SELECT = 'FLOWS_SELECT'
export const SET_FILTER = 'FLOWS_SET_FILTER'
export const SET_SORT = 'FLOWS_SET_SORT'
export const SET_HIGHLIGHT = 'FLOWS_SET_HIGHLIGHT'
export const REQUEST_ACTION = 'FLOWS_REQUEST_ACTION' export const REQUEST_ACTION = 'FLOWS_REQUEST_ACTION'
export const UNKNOWN_CMD = 'FLOWS_UNKNOWN_CMD'
export const FETCH_ERROR = 'FLOWS_FETCH_ERROR'
export const SELECT = 'FLOWS_SELECT'
const defaultState = { const defaultState = {
highlight: null,
filter: null,
sort: { column: null, desc: false },
selected: [], selected: [],
...reduceList(undefined, {}), ...reduceStore(undefined, {})
} }
export default function reduce(state = defaultState, action) { export default function reduce(state = defaultState, action) {
switch (action.type) { switch (action.type) {
case ADD: case ADD:
return {
...state,
...reduceList(state, listActions.add(action.item)),
}
case UPDATE: case UPDATE:
return {
...state,
...reduceList(state, listActions.update(action.item)),
}
case REMOVE: case REMOVE:
case RECEIVE:
// FIXME: Implement select switch for remove
let storeAction = storeActions[action.cmd](
action.data,
makeFilter(state.filter),
makeSort(state.sort)
)
return { return {
...state, ...state,
...reduceList(state, listActions.remove(action.id)), ...reduceStore(state, storeAction)
} }
case RECEIVE: case SET_FILTER:
return { return {
...state, ...state,
...reduceList(state, listActions.receive(action.flows)), filter: action.filter,
...reduceStore(state, storeActions.setFilter(makeFilter(action.filter), makeSort(state.sort)))
}
case SET_SORT:
return {
...state,
sort: action.sort,
...reduceStore(state, storeActions.setSort(makeSort(action.sort)))
} }
case SELECT: case SELECT:
@ -51,13 +62,88 @@ export default function reduce(state = defaultState, action) {
} }
default: default:
return { return state
...state,
...reduceList(state, action),
}
} }
} }
const sortKeyFuns = {
TLSColumn: flow => flow.request.scheme,
PathColumn: flow => RequestUtils.pretty_url(flow.request),
MethodColumn: flow => flow.request.method,
StatusColumn: flow => flow.response && flow.response.status_code,
TimeColumn: flow => flow.response && flow.response.timestamp_end - flow.request.timestamp_start,
SizeColumn: flow => {
let total = flow.request.contentLength
if (flow.response) {
total += flow.response.contentLength || 0
}
return total
},
}
export function makeFilter(filter) {
if (!filter) {
return
}
return Filt.parse(filter)
}
export function makeSort({ column, desc }) {
const sortKeyFun = sortKeyFuns[column]
if (!sortKeyFun) {
return
}
return (a, b) => {
const ka = sortKeyFun(a)
const kb = sortKeyFun(b)
if (ka > kb) {
return desc ? -1 : 1
}
if (ka < kb) {
return desc ? 1 : -1
}
return 0
}
}
export function setFilter(filter) {
return { type: SET_FILTER, filter }
}
export function setHighlight(highlight) {
return { type: SET_HIGHLIGHT, highlight }
}
export function setSort(column, desc) {
return { type: SET_SORT, sort: { column, desc } }
}
export function selectRelative(shift) {
return (dispatch, getState) => {
let currentSelectionIndex = getState().flows.viewIndex[getState().flows.selected[0]]
let minIndex = 0
let maxIndex = getState().flows.view.length - 1
let newIndex
if (currentSelectionIndex === undefined) {
newIndex = (shift < 0) ? minIndex : maxIndex
} else {
newIndex = currentSelectionIndex + shift
newIndex = window.Math.max(newIndex, minIndex)
newIndex = window.Math.min(newIndex, maxIndex)
}
let flow = getState().flows.view[newIndex]
dispatch(select(flow ? flow.id : undefined))
}
}
export function accept(flow) { export function accept(flow) {
return dispatch => fetchApi(`/flows/${flow.id}/accept`, { method: 'POST' }) return dispatch => fetchApi(`/flows/${flow.id}/accept`, { method: 'POST' })
} }
@ -88,9 +174,9 @@ export function update(flow, data) {
export function uploadContent(flow, file, type) { export function uploadContent(flow, file, type) {
const body = new FormData() const body = new FormData()
file = new Blob([file], {type: 'plain/text'}) file = new window.Blob([file], { type: 'plain/text' })
body.append('file', file) body.append('file', file)
return dispatch => fetchApi(`/flows/${flow.id}/${type}/content`, {method: 'post', body} ) return dispatch => fetchApi(`/flows/${flow.id}/${type}/content`, { method: 'post', body })
} }

View File

@ -1,18 +1,12 @@
import { combineReducers } from 'redux' import { combineReducers } from 'redux'
import eventLog from './eventLog' import eventLog from './eventLog'
import websocket from './websocket'
import flows from './flows' import flows from './flows'
import flowView from './flowView'
import settings from './settings' import settings from './settings'
import ui from './ui/index' import ui from './ui/index'
import msgQueue from './msgQueue'
export default combineReducers({ export default combineReducers({
eventLog, eventLog,
websocket,
flows, flows,
flowView,
settings, settings,
ui, ui,
msgQueue,
}) })

View File

@ -13,7 +13,7 @@ export default function reducer(state = defaultState, action) {
switch (action.type) { switch (action.type) {
case RECEIVE: case RECEIVE:
return action.settings return action.data
case UPDATE: case UPDATE:
return { return {

View File

@ -60,7 +60,7 @@ export default function reducer(state = defaultState, action) {
// There is no explicit "stop edit" event. // There is no explicit "stop edit" event.
// We stop editing when we receive an update for // We stop editing when we receive an update for
// the currently edited flow from the server // the currently edited flow from the server
if (action.item.id === state.modifiedFlow.id) { if (action.data.id === state.modifiedFlow.id) {
return { return {
...state, ...state,
modifiedFlow: false, modifiedFlow: false,

View File

@ -1,5 +1,4 @@
import { Key } from '../../utils' import { Key } from '../../utils'
import { selectRelative as selectFlowRelative } from '../flowView'
import { selectTab } from './flow' import { selectTab } from './flow'
import * as flowsActions from '../flows' import * as flowsActions from '../flows'
@ -20,29 +19,29 @@ export function onKeyDown(e) {
switch (key) { switch (key) {
case Key.K: case Key.K:
case Key.UP: case Key.UP:
dispatch(selectFlowRelative(-1)) dispatch(flowsActions.selectRelative(-1))
break break
case Key.J: case Key.J:
case Key.DOWN: case Key.DOWN:
dispatch(selectFlowRelative(+1)) dispatch(flowsActions.selectRelative(+1))
break break
case Key.SPACE: case Key.SPACE:
case Key.PAGE_DOWN: case Key.PAGE_DOWN:
dispatch(selectFlowRelative(+10)) dispatch(flowsActions.selectRelative(+10))
break break
case Key.PAGE_UP: case Key.PAGE_UP:
dispatch(selectFlowRelative(-10)) dispatch(flowsActions.selectRelative(-10))
break break
case Key.END: case Key.END:
dispatch(selectFlowRelative(+1e10)) dispatch(flowsActions.selectRelative(+1e10))
break break
case Key.HOME: case Key.HOME:
dispatch(selectFlowRelative(-1e10)) dispatch(flowsActions.selectRelative(-1e10))
break break
case Key.ESC: case Key.ESC:

View File

@ -1,93 +0,0 @@
import _ from 'lodash'
export const ADD = 'LIST_ADD'
export const UPDATE = 'LIST_UPDATE'
export const REMOVE = 'LIST_REMOVE'
export const RECEIVE = 'LIST_RECEIVE'
const defaultState = {
data: [],
byId: {},
indexOf: {},
}
export default function reduce(state = defaultState, action) {
switch (action.type) {
case ADD:
return {
...state,
data: [...state.data, action.item],
byId: { ...state.byId, [action.item.id]: action.item },
indexOf: { ...state.indexOf, [action.item.id]: state.data.length },
}
case UPDATE: {
const index = state.indexOf[action.item.id]
if (index == null) {
return state
}
const data = [...state.data]
data[index] = action.item
return {
...state,
data,
byId: { ...state.byId, [action.item.id]: action.item }
}
}
case REMOVE: {
const index = state.indexOf[action.id]
if (index == null) {
return state
}
const data = [...state.data]
const indexOf = { ...state.indexOf, [action.id]: null }
data.splice(index, 1)
for (let i = data.length - 1; i >= index; i--) {
indexOf[data[i].id] = i
}
return {
...state,
data,
indexOf,
byId: { ...state.byId, [action.id]: null },
}
}
case RECEIVE:
return {
...state,
data: action.list,
byId: _.fromPairs(action.list.map(item => [item.id, item])),
indexOf: _.fromPairs(action.list.map((item, index) => [item.id, index])),
}
default:
return state
}
}
export function add(item) {
return { type: ADD, item }
}
export function update(item) {
return { type: UPDATE, item }
}
export function remove(id) {
return { type: REMOVE, id }
}
export function receive(list) {
return { type: RECEIVE, list }
}

View File

@ -0,0 +1,194 @@
export const SET_FILTER = 'LIST_SET_FILTER'
export const SET_SORT = 'LIST_SET_SORT'
export const ADD = 'LIST_ADD'
export const UPDATE = 'LIST_UPDATE'
export const REMOVE = 'LIST_REMOVE'
export const RECEIVE = 'LIST_RECEIVE'
const defaultState = {
byId: {},
list: [],
listIndex: {},
view: [],
viewIndex: {},
}
export default function reduce(state = defaultState, action) {
let { byId, list, listIndex, view, viewIndex } = state
switch (action.type) {
case SET_FILTER:
view = list.filter(action.filter).sort(action.sort)
viewIndex = {}
view.forEach((item, index) => {
viewIndex[item.id] = index
})
break
case SET_SORT:
view = [...view].sort(action.sort)
viewIndex = {}
view.forEach((item, index) => {
viewIndex[item.id] = index
})
break
case ADD:
if (action.item.id in byId) {
// we already had that.
break
}
byId = { ...byId, [action.item.id]: action.item }
listIndex = { ...listIndex, [action.item.id]: list.length }
list = [...list, action.item]
if (action.filter(action.item)) {
({ view, viewIndex } = sortedInsert(state, action.item, action.sort))
}
break
case UPDATE:
byId = { ...byId, [action.item.id]: action.item }
list = [...list]
list[listIndex[action.item.id]] = action.item
let hasOldItem = action.item.id in viewIndex
let hasNewItem = action.filter(action.item)
if (hasNewItem && !hasOldItem) {
({view, viewIndex} = sortedInsert(state, action.item, action.sort))
}
else if (!hasNewItem && hasOldItem) {
({data: view, dataIndex: viewIndex} = removeData(view, viewIndex, action.item.id))
}
else if (hasNewItem && hasOldItem) {
({view, viewIndex} = sortedUpdate(state, action.item, action.sort))
}
break
case REMOVE:
if (!(action.id in byId)) {
break
}
delete byId[action.id];
({data: list, dataIndex: listIndex} = removeData(list, listIndex, action.id))
if (action.id in viewIndex) {
({data: view, dataIndex: viewIndex} = removeData(view, viewIndex, action.id))
}
break
case RECEIVE:
list = action.list
listIndex = {}
byId = {}
list.forEach((item, i) => {
byId[item.id] = item
listIndex[item.id] = i
})
view = list.filter(action.filter).sort(action.sort)
viewIndex = {}
view.forEach((item, index) => {
viewIndex[item.id] = index
})
break
}
return { byId, list, listIndex, view, viewIndex }
}
export function setFilter(filter = defaultFilter, sort = defaultSort) {
return { type: SET_FILTER, filter, sort }
}
export function setSort(sort = defaultSort) {
return { type: SET_SORT, sort }
}
export function add(item, filter = defaultFilter, sort = defaultSort) {
return { type: ADD, item, filter, sort }
}
export function update(item, filter = defaultFilter, sort = defaultSort) {
return { type: UPDATE, item, filter, sort }
}
export function remove(id) {
return { type: REMOVE, id }
}
export function receive(list, filter = defaultFilter, sort = defaultSort) {
return { type: RECEIVE, list, filter, sort }
}
function sortedInsert(state, item, sort) {
const index = sortedIndex(state.view, item, sort)
const view = [...state.view]
const viewIndex = { ...state.viewIndex }
view.splice(index, 0, item)
for (let i = view.length - 1; i >= index; i--) {
viewIndex[view[i].id] = i
}
return { view, viewIndex }
}
function removeData(currentData, currentDataIndex, id) {
const index = currentDataIndex[id]
const data = [...currentData]
const dataIndex = { ...currentDataIndex }
delete dataIndex[id];
data.splice(index, 1)
for (let i = data.length - 1; i >= index; i--) {
dataIndex[data[i].id] = i
}
return { data, dataIndex }
}
function sortedUpdate(state, item, sort) {
let view = [...state.view]
let viewIndex = { ...state.viewIndex }
let index = viewIndex[item.id]
view[index] = item
while (index + 1 < view.length && sort(view[index], view[index + 1]) > 0) {
view[index] = view[index + 1]
view[index + 1] = item
viewIndex[item.id] = index + 1
viewIndex[view[index].id] = index
++index
}
while (index > 0 && sort(view[index], view[index - 1]) < 0) {
view[index] = view[index - 1]
view[index - 1] = item
viewIndex[item.id] = index - 1
viewIndex[view[index].id] = index
--index
}
return { view, viewIndex }
}
function sortedIndex(list, item, sort) {
let low = 0
let high = list.length
while (low < high) {
const middle = (low + high) >>> 1
if (sort(item, list[middle]) >= 0) {
low = middle + 1
} else {
high = middle
}
}
return low
}
function defaultFilter() {
return true
}
function defaultSort(a, b) {
return 0
}

View File

@ -1,189 +0,0 @@
import _ from 'lodash'
export const UPDATE_FILTER = 'VIEW_UPDATE_FILTER'
export const UPDATE_SORT = 'VIEW_UPDATE_SORT'
export const ADD = 'VIEW_ADD'
export const UPDATE = 'VIEW_UPDATE'
export const REMOVE = 'VIEW_REMOVE'
export const RECEIVE = 'VIEW_RECEIVE'
const defaultState = {
data: [],
indexOf: {},
}
export default function reduce(state = defaultState, action) {
switch (action.type) {
case UPDATE_FILTER:
{
const data = action.list.filter(action.filter).sort(action.sort)
return {
...state,
data,
indexOf: _.fromPairs(data.map((item, index) => [item.id, index])),
}
}
case UPDATE_SORT:
{
const data = [...state.data].sort(action.sort)
return {
...state,
data,
indexOf: _.fromPairs(data.map((item, index) => [item.id, index])),
}
}
case ADD:
if (state.indexOf[action.item.id] != null || !action.filter(action.item)) {
return state
}
return {
...state,
...sortedInsert(state, action.item, action.sort),
}
case REMOVE:
if (state.indexOf[action.id] == null) {
return state
}
return {
...state,
...sortedRemove(state, action.id),
}
case UPDATE:
let hasOldItem = state.indexOf[action.item.id] !== null && state.indexOf[action.item.id] !== undefined
let hasNewItem = action.filter(action.item)
if (!hasNewItem && !hasOldItem) {
return state
}
if (hasNewItem && !hasOldItem) {
return {
...state,
...sortedInsert(state, action.item, action.sort)
}
}
if (!hasNewItem && hasOldItem) {
return {
...state,
...sortedRemove(state, action.item.id)
}
}
if (hasNewItem && hasOldItem) {
return {
...state,
...sortedUpdate(state, action.item, action.sort),
}
}
case RECEIVE:
{
const data = action.list.filter(action.filter).sort(action.sort)
return {
...state,
data,
indexOf: _.fromPairs(data.map((item, index) => [item.id, index])),
}
}
default:
return state
}
}
export function updateFilter(list, filter = defaultFilter, sort = defaultSort) {
return { type: UPDATE_FILTER, list, filter, sort }
}
export function updateSort(sort = defaultSort) {
return { type: UPDATE_SORT, sort }
}
export function add(item, filter = defaultFilter, sort = defaultSort) {
return { type: ADD, item, filter, sort }
}
export function update(item, filter = defaultFilter, sort = defaultSort) {
return { type: UPDATE, item, filter, sort }
}
export function remove(id) {
return { type: REMOVE, id }
}
export function receive(list, filter = defaultFilter, sort = defaultSort) {
return { type: RECEIVE, list, filter, sort }
}
function sortedInsert(state, item, sort) {
const index = sortedIndex(state.data, item, sort)
const data = [ ...state.data ]
const indexOf = { ...state.indexOf }
data.splice(index, 0, item)
for (let i = data.length - 1; i >= index; i--) {
indexOf[data[i].id] = i
}
return { data, indexOf }
}
function sortedRemove(state, id) {
const index = state.indexOf[id]
const data = [...state.data]
const indexOf = { ...state.indexOf, [id]: null }
data.splice(index, 1)
for (let i = data.length - 1; i >= index; i--) {
indexOf[data[i].id] = i
}
return { data, indexOf }
}
function sortedUpdate(state, item, sort) {
let data = [ ...state.data ]
let indexOf = { ...state.indexOf }
let index = indexOf[item.id]
data[index] = item
while (index + 1 < data.length && sort(data[index], data[index + 1]) > 0) {
data[index] = data[index + 1]
data[index + 1] = item
indexOf[item.id] = index + 1
indexOf[data[index].id] = index
++index
}
while (index > 0 && sort(data[index], data[index - 1]) < 0) {
data[index] = data[index - 1]
data[index - 1] = item
indexOf[item.id] = index - 1
indexOf[data[index].id] = index
--index
}
return { data, indexOf }
}
function sortedIndex(list, item, sort) {
let low = 0
let high = list.length
while (low < high) {
const middle = (low + high) >>> 1
if (sort(item, list[middle]) >= 0) {
low = middle + 1
} else {
high = middle
}
}
return low
}
function defaultFilter() {
return true
}
function defaultSort(a, b) {
return 0
}

View File

@ -1,6 +1,5 @@
import { select } from "./ducks/flows" import { select, setFilter, setHighlight } from "./ducks/flows"
import { selectTab } from "./ducks/ui/flow" import { selectTab } from "./ducks/ui/flow"
import { updateFilter, updateHighlight } from "./ducks/flowView"
import { toggleVisibility } from "./ducks/eventLog" import { toggleVisibility } from "./ducks/eventLog"
const Query = { const Query = {
@ -10,7 +9,7 @@ const Query = {
}; };
function updateStoreFromUrl(store) { function updateStoreFromUrl(store) {
const [path, query] = window.location.hash.substr(1).split("?", 2) const [path, query] = window.location.hash.substr(1).split("?", 2)
const path_components = path.substr(1).split("/") const path_components = path.substr(1).split("/")
if (path_components[0] === "flows") { if (path_components[0] === "flows") {
@ -28,14 +27,14 @@ function updateStoreFromUrl(store) {
const [key, value] = x.split("=", 2) const [key, value] = x.split("=", 2)
switch (key) { switch (key) {
case Query.SEARCH: case Query.SEARCH:
store.dispatch(updateFilter(value)) store.dispatch(setFilter(value))
break break
case Query.HIGHLIGHT: case Query.HIGHLIGHT:
store.dispatch(updateHighlight(value)) store.dispatch(setHighlight(value))
break break
case Query.SHOW_EVENTLOG: case Query.SHOW_EVENTLOG:
if(!store.getState().eventLog.visible) if (!store.getState().eventLog.visible)
store.dispatch(toggleVisibility(value)) store.dispatch(toggleVisibility())
break break
default: default:
console.error(`unimplemented query arg: ${x}`) console.error(`unimplemented query arg: ${x}`)
@ -45,10 +44,10 @@ function updateStoreFromUrl(store) {
} }
function updateUrlFromStore(store) { function updateUrlFromStore(store) {
const state = store.getState() const state = store.getState()
let query = { let query = {
[Query.SEARCH]: state.flowView.filter, [Query.SEARCH]: state.flows.filter,
[Query.HIGHLIGHT]: state.flowView.highlight, [Query.HIGHLIGHT]: state.flows.highlight,
[Query.SHOW_EVENTLOG]: state.eventLog.visible, [Query.SHOW_EVENTLOG]: state.eventLog.visible,
} }
const queryStr = Object.keys(query) const queryStr = Object.keys(query)