aboutsummaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/src/js/__tests__/ducks/ui.js5
-rw-r--r--web/src/js/actions.js55
-rw-r--r--web/src/js/app.jsx4
-rw-r--r--web/src/js/components/EventLog.jsx10
-rw-r--r--web/src/js/components/FlowTable/FlowColumns.jsx8
-rw-r--r--web/src/js/components/FlowTable/FlowTableHead.jsx10
-rw-r--r--web/src/js/components/Header.jsx7
-rw-r--r--web/src/js/components/Header/FileMenu.jsx8
-rw-r--r--web/src/js/components/Header/FlowMenu.jsx2
-rw-r--r--web/src/js/components/Header/ViewMenu.jsx4
-rw-r--r--web/src/js/components/MainView.jsx21
-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
18 files changed, 805 insertions, 487 deletions
diff --git a/web/src/js/__tests__/ducks/ui.js b/web/src/js/__tests__/ducks/ui.js
index 81ae852c..44a71aa3 100644
--- a/web/src/js/__tests__/ducks/ui.js
+++ b/web/src/js/__tests__/ducks/ui.js
@@ -1,8 +1,9 @@
jest.unmock("../../ducks/ui");
+// @todo fix it ( this is why I don't like to add tests until our architecture is stable :P )
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 643bb8b7..23297f63 100644
--- a/web/src/js/app.jsx
+++ b/web/src/js/app.jsx
@@ -9,7 +9,7 @@ import { Route, Router as ReactRouter, hashHistory, Redirect } from 'react-route
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(
// @todo move to ProxyApp
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..e426672a 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'
@@ -69,11 +69,11 @@ class EventLog extends Component {
export default connect(
state => ({
- filters: state.eventLog.filter,
- events: state.eventLog.filteredEvents,
+ filters: state.eventLog.filters,
+ events: state.eventLog.view.data,
}),
{
- onClose: toggleEventLogVisibility,
- onToggleFilter: toggleEventLogFilter,
+ onClose: toggleVisibility,
+ onToggleFilter: toggleFilter,
}
)(EventLog)
diff --git a/web/src/js/components/FlowTable/FlowColumns.jsx b/web/src/js/components/FlowTable/FlowColumns.jsx
index 11c0796c..0ff80453 100644
--- a/web/src/js/components/FlowTable/FlowColumns.jsx
+++ b/web/src/js/components/FlowTable/FlowColumns.jsx
@@ -9,7 +9,6 @@ export function TLSColumn({ flow }) {
)
}
-TLSColumn.sortKeyFun = flow => flow.request.scheme
TLSColumn.headerClass = 'col-tls'
TLSColumn.headerName = ''
@@ -68,7 +67,6 @@ export function PathColumn({ flow }) {
)
}
-PathColumn.sortKeyFun = flow => RequestUtils.pretty_url(flow.request)
PathColumn.headerClass = 'col-path'
PathColumn.headerName = 'Path'
@@ -78,7 +76,6 @@ export function MethodColumn({ flow }) {
)
}
-MethodColumn.sortKeyFun = flow => flow.request.method
MethodColumn.headerClass = 'col-method'
MethodColumn.headerName = 'Method'
@@ -88,7 +85,6 @@ export function StatusColumn({ flow }) {
)
}
-StatusColumn.sortKeyFun = flow => flow.response && flow.response.status_code
StatusColumn.headerClass = 'col-status'
StatusColumn.headerName = 'Status'
@@ -98,7 +94,7 @@ export function SizeColumn({ flow }) {
)
}
-SizeColumn.sortKeyFun = flow => {
+SizeColumn.getTotalSize = flow => {
let total = flow.request.contentLength
if (flow.response) {
total += flow.response.contentLength || 0
@@ -106,7 +102,6 @@ SizeColumn.sortKeyFun = flow => {
return total
}
-SizeColumn.getTotalSize = SizeColumn.sortKeyFun
SizeColumn.headerClass = 'col-size'
SizeColumn.headerName = 'Size'
@@ -122,7 +117,6 @@ export function TimeColumn({ flow }) {
)
}
-TimeColumn.sortKeyFun = flow => flow.response && flow.response.timestamp_end - flow.request.timestamp_start
TimeColumn.headerClass = 'col-time'
TimeColumn.headerName = 'Time'
diff --git a/web/src/js/components/FlowTable/FlowTableHead.jsx b/web/src/js/components/FlowTable/FlowTableHead.jsx
index 840f6a34..6deee808 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/views/main'
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.headerName}
</th>
))}
@@ -29,10 +29,10 @@ function FlowTableHead({ sortColumn, sortDesc, onSort }) {
export default connect(
state => ({
- sortDesc: state.flows.sort.sortDesc,
- sortColumn: state.flows.sort.sortColumn,
+ sortDesc: state.flows.views.main.sorter.desc,
+ sortColumn: state.flows.views.main.sorter.column,
}),
{
- onSort: setSort,
+ onSort: updateSorter,
}
)(FlowTableHead)
diff --git a/web/src/js/components/Header.jsx b/web/src/js/components/Header.jsx
index ab25eb41..545684bb 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'
@@ -52,9 +51,9 @@ class Header extends Component {
}
}
export default connect(
- (state) => ({
- selectedFlow: state.flows.selected[0],
- activeMenu: state.ui.activeMenu
+ state => ({
+ selectedFlow: state.flows.views.main.selected[0],
+ activeMenu: state.ui.activeMenu,
}),
{
setActiveMenu,
diff --git a/web/src/js/components/Header/FileMenu.jsx b/web/src/js/components/Header/FileMenu.jsx
index b075b3c8..e1c62e28 100644
--- a/web/src/js/components/Header/FileMenu.jsx
+++ b/web/src/js/components/Header/FileMenu.jsx
@@ -1,6 +1,6 @@
import React, { Component } from 'react'
import classnames from 'classnames'
-import { FlowActions } from '../../actions.js'
+import * as flowActions from '../../ducks/flows'
export default class FileMenu extends Component {
@@ -35,7 +35,7 @@ export default class FileMenu extends Component {
onNewClick(e) {
e.preventDefault()
if (confirm('Delete all flows?')) {
- FlowActions.clear()
+ flowActions.clear()
}
}
@@ -47,14 +47,14 @@ export default class FileMenu extends Component {
onOpenFile(e) {
e.preventDefault()
if (e.target.files.length > 0) {
- FlowActions.upload(e.target.files[0])
+ flowActions.upload(e.target.files[0])
this.fileInput.value = ''
}
}
onSaveClick(e) {
e.preventDefault()
- FlowActions.download()
+ flowActions.download()
}
render() {
diff --git a/web/src/js/components/Header/FlowMenu.jsx b/web/src/js/components/Header/FlowMenu.jsx
index abecf0dc..689cea5c 100644
--- a/web/src/js/components/Header/FlowMenu.jsx
+++ b/web/src/js/components/Header/FlowMenu.jsx
@@ -29,6 +29,6 @@ function FlowMenu({ flow }) {
export default connect(
state => ({
- flow: state.flows.all.byId[state.flows.selected[0]]
+ flow: state.flows.list.data[state.flows.views.main.selected[0]],
})
)(FlowMenu)
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..b3fe73ec 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/views/main'
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)
}
}
@@ -154,7 +154,7 @@ class MainView extends Component {
}
render() {
- const { flows, selectedFlow, highlight, sort } = this.props
+ const { flows, selectedFlow, highlight } = this.props
return (
<div className="main-view">
<FlowTable
@@ -182,16 +182,15 @@ class MainView extends Component {
export default connect(
state => ({
- flows: state.flows.view,
- filter: state.flows.filter,
- sort: state.flows.sort,
- highlight: state.flows.highlight,
- selectedFlow: state.flows.all.byId[state.flows.selected[0]]
+ flows: state.flows.views.main.view.data,
+ filter: state.flows.views.main.filter,
+ highlight: state.flows.views.main.highlight,
+ selectedFlow: state.flows.list.data[state.flows.views.main.selected[0]]
}),
{
selectFlow,
- setFilter,
- setHighlight,
+ updateFilter,
+ updateHighlight,
},
undefined,
{ withRef: true }
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)