aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/js')
-rw-r--r--web/src/js/__tests__/ducks/connectionSpec.js41
-rw-r--r--web/src/js/__tests__/urlStateSpec.js100
-rw-r--r--web/src/js/backends/websocket.js19
-rw-r--r--web/src/js/components/Header.jsx6
-rw-r--r--web/src/js/components/Header/ConnectionIndicator.jsx30
-rw-r--r--web/src/js/components/common/DocsLink.jsx2
-rw-r--r--web/src/js/ducks/connection.js44
-rw-r--r--web/src/js/ducks/index.js12
-rw-r--r--web/src/js/ducks/ui/flow.js6
-rw-r--r--web/src/js/ducks/ui/header.js2
-rw-r--r--web/src/js/urlState.js4
11 files changed, 247 insertions, 19 deletions
diff --git a/web/src/js/__tests__/ducks/connectionSpec.js b/web/src/js/__tests__/ducks/connectionSpec.js
new file mode 100644
index 00000000..d087e867
--- /dev/null
+++ b/web/src/js/__tests__/ducks/connectionSpec.js
@@ -0,0 +1,41 @@
+import reduceConnection from "../../ducks/connection"
+import * as ConnectionActions from "../../ducks/connection"
+import { ConnectionState } from "../../ducks/connection"
+
+describe('connection reducer', () => {
+ it('should return initial state', () => {
+ expect(reduceConnection(undefined, {})).toEqual({
+ state: ConnectionState.INIT,
+ message: null,
+ })
+ })
+
+ it('should handle start fetch', () => {
+ expect(reduceConnection(undefined, ConnectionActions.startFetching())).toEqual({
+ state: ConnectionState.FETCHING,
+ message: undefined,
+ })
+ })
+
+ it('should handle connection established', () => {
+ expect(reduceConnection(undefined, ConnectionActions.connectionEstablished())).toEqual({
+ state: ConnectionState.ESTABLISHED,
+ message: undefined,
+ })
+ })
+
+ it('should handle connection error', () => {
+ expect(reduceConnection(undefined, ConnectionActions.connectionError("no internet"))).toEqual({
+ state: ConnectionState.ERROR,
+ message: "no internet",
+ })
+ })
+
+ it('should handle offline mode', () => {
+ expect(reduceConnection(undefined, ConnectionActions.setOffline())).toEqual({
+ state: ConnectionState.OFFLINE,
+ message: undefined,
+ })
+ })
+
+})
diff --git a/web/src/js/__tests__/urlStateSpec.js b/web/src/js/__tests__/urlStateSpec.js
new file mode 100644
index 00000000..c57c0a00
--- /dev/null
+++ b/web/src/js/__tests__/urlStateSpec.js
@@ -0,0 +1,100 @@
+import initialize from '../urlState'
+import { updateStoreFromUrl, updateUrlFromStore } from '../urlState'
+
+import reduceFlows from '../ducks/flows'
+import reduceUI from '../ducks/ui/index'
+import reduceEventLog from '../ducks/eventLog'
+import * as flowsActions from '../ducks/flows'
+
+import configureStore from 'redux-mock-store'
+
+const mockStore = configureStore()
+history.replaceState = jest.fn()
+
+describe('updateStoreFromUrl', () => {
+
+ it('should handle search query', () => {
+ window.location.hash = "#/flows?s=foo"
+ let store = mockStore()
+ updateStoreFromUrl(store)
+ expect(store.getActions()).toEqual([{ filter: "foo", type: "FLOWS_SET_FILTER" }])
+ })
+
+ it('should handle highlight query', () => {
+ window.location.hash = "#/flows?h=foo"
+ let store = mockStore()
+ updateStoreFromUrl(store)
+ expect(store.getActions()).toEqual([{ highlight: "foo", type: "FLOWS_SET_HIGHLIGHT" }])
+ })
+
+ it('should handle show event log', () => {
+ window.location.hash = "#/flows?e=true"
+ let initialState = { eventLog: reduceEventLog(undefined, {}) },
+ store = mockStore(initialState)
+ updateStoreFromUrl(store)
+ expect(store.getActions()).toEqual([{ type: "EVENTS_TOGGLE_VISIBILITY" }])
+ })
+
+ it('should handle unimplemented query argument', () => {
+ window.location.hash = "#/flows?foo=bar"
+ console.error = jest.fn()
+ let store = mockStore()
+ updateStoreFromUrl(store)
+ expect(console.error).toBeCalledWith("unimplemented query arg: foo=bar")
+ })
+
+ it('should select flow and tab', () => {
+ window.location.hash = "#/flows/123/request"
+ let store = mockStore()
+ updateStoreFromUrl(store)
+ expect(store.getActions()).toEqual([
+ {
+ flowIds: ["123"],
+ type: "FLOWS_SELECT"
+ },
+ {
+ tab: "request",
+ type: "UI_FLOWVIEW_SET_TAB"
+ }
+ ])
+ })
+})
+
+describe('updateUrlFromStore', () => {
+ let initialState = {
+ flows: reduceFlows(undefined, {}),
+ ui: reduceUI(undefined, {}),
+ eventLog: reduceEventLog(undefined, {})
+ }
+
+ it('should update initial url', () => {
+ let store = mockStore(initialState)
+ updateUrlFromStore(store)
+ expect(history.replaceState).toBeCalledWith(undefined, '', '/#/flows')
+ })
+
+ it('should update url', () => {
+ let flows = reduceFlows(undefined, flowsActions.select(123)),
+ state = {
+ ...initialState,
+ flows: reduceFlows(flows, flowsActions.setFilter('~u foo'))
+ },
+ store = mockStore(state)
+ updateUrlFromStore(store)
+ expect(history.replaceState).toBeCalledWith(undefined, '', '/#/flows/123/request?s=~u foo')
+ })
+})
+
+describe('initialize', () => {
+ let initialState = {
+ flows: reduceFlows(undefined, {}),
+ ui: reduceUI(undefined, {}),
+ eventLog: reduceEventLog(undefined, {})
+ }
+
+ it('should handle initial state', () => {
+ let store = mockStore(initialState)
+ initialize(store)
+ store.dispatch({ type: "foo" })
+ })
+})
diff --git a/web/src/js/backends/websocket.js b/web/src/js/backends/websocket.js
index 44b260c9..01094ac4 100644
--- a/web/src/js/backends/websocket.js
+++ b/web/src/js/backends/websocket.js
@@ -4,6 +4,7 @@
* An alternative backend may use the REST API only to host static instances.
*/
import { fetchApi } from "../utils"
+import * as connectionActions from "../ducks/connection"
const CMD_RESET = 'reset'
@@ -17,7 +18,7 @@ export default class WebsocketBackend {
connect() {
this.socket = new WebSocket(location.origin.replace('http', 'ws') + '/updates')
this.socket.addEventListener('open', () => this.onOpen())
- this.socket.addEventListener('close', () => this.onClose())
+ this.socket.addEventListener('close', event => this.onClose(event))
this.socket.addEventListener('message', msg => this.onMessage(JSON.parse(msg.data)))
this.socket.addEventListener('error', error => this.onError(error))
}
@@ -26,6 +27,7 @@ export default class WebsocketBackend {
this.fetchData("settings")
this.fetchData("flows")
this.fetchData("events")
+ this.store.dispatch(connectionActions.startFetching())
}
fetchData(resource) {
@@ -59,15 +61,22 @@ export default class WebsocketBackend {
let queue = this.activeFetches[resource]
delete this.activeFetches[resource]
queue.forEach(msg => this.onMessage(msg))
+
+ if(Object.keys(this.activeFetches).length === 0) {
+ // We have fetched the last resource
+ this.store.dispatch(connectionActions.connectionEstablished())
+ }
}
- onClose() {
- // FIXME
- console.error("onClose", arguments)
+ onClose(closeEvent) {
+ this.store.dispatch(connectionActions.connectionError(
+ `Connection closed at ${new Date().toUTCString()} with error code ${closeEvent.code}.`
+ ))
+ console.error("websocket connection closed", closeEvent)
}
onError() {
// FIXME
- console.error("onError", arguments)
+ console.error("websocket connection errored", arguments)
}
}
diff --git a/web/src/js/components/Header.jsx b/web/src/js/components/Header.jsx
index f362e4a1..ebe7453c 100644
--- a/web/src/js/components/Header.jsx
+++ b/web/src/js/components/Header.jsx
@@ -7,6 +7,7 @@ import OptionMenu from './Header/OptionMenu'
import FileMenu from './Header/FileMenu'
import FlowMenu from './Header/FlowMenu'
import {setActiveMenu} from '../ducks/ui/header'
+import ConnectionIndicator from "./Header/ConnectionIndicator"
class Header extends Component {
static entries = [MainMenu, OptionMenu]
@@ -39,10 +40,11 @@ class Header extends Component {
{Entry.title}
</a>
))}
+ <ConnectionIndicator/>
</nav>
- <menu>
+ <div>
<Active/>
- </menu>
+ </div>
</header>
)
}
diff --git a/web/src/js/components/Header/ConnectionIndicator.jsx b/web/src/js/components/Header/ConnectionIndicator.jsx
new file mode 100644
index 00000000..1ee42e25
--- /dev/null
+++ b/web/src/js/components/Header/ConnectionIndicator.jsx
@@ -0,0 +1,30 @@
+import React from "react"
+import PropTypes from "prop-types"
+import { connect } from "react-redux"
+import { ConnectionState } from "../../ducks/connection"
+
+
+ConnectionIndicator.propTypes = {
+ state: PropTypes.symbol.isRequired,
+ message: PropTypes.string,
+
+}
+function ConnectionIndicator({ state, message }) {
+ switch (state) {
+ case ConnectionState.INIT:
+ return <span className="connection-indicator init">connecting…</span>;
+ case ConnectionState.FETCHING:
+ return <span className="connection-indicator fetching">fetching data…</span>;
+ case ConnectionState.ESTABLISHED:
+ return <span className="connection-indicator established">connected</span>;
+ case ConnectionState.ERROR:
+ return <span className="connection-indicator error"
+ title={message}>connection lost</span>;
+ case ConnectionState.OFFLINE:
+ return <span className="connection-indicator offline">offline</span>;
+ }
+}
+
+export default connect(
+ state => state.connection,
+)(ConnectionIndicator)
diff --git a/web/src/js/components/common/DocsLink.jsx b/web/src/js/components/common/DocsLink.jsx
index 182811a3..53c7aca8 100644
--- a/web/src/js/components/common/DocsLink.jsx
+++ b/web/src/js/components/common/DocsLink.jsx
@@ -1,4 +1,4 @@
-import { PropTypes } from 'react'
+import PropTypes from "prop-types"
DocsLink.propTypes = {
resource: PropTypes.string.isRequired,
diff --git a/web/src/js/ducks/connection.js b/web/src/js/ducks/connection.js
new file mode 100644
index 00000000..ffa2c309
--- /dev/null
+++ b/web/src/js/ducks/connection.js
@@ -0,0 +1,44 @@
+export const ConnectionState = {
+ INIT: Symbol("init"),
+ FETCHING: Symbol("fetching"), // WebSocket is established, but still startFetching resources.
+ ESTABLISHED: Symbol("established"),
+ ERROR: Symbol("error"),
+ OFFLINE: Symbol("offline"), // indicates that there is no live (websocket) backend.
+}
+
+const defaultState = {
+ state: ConnectionState.INIT,
+ message: null,
+}
+
+export default function reducer(state = defaultState, action) {
+ switch (action.type) {
+
+ case ConnectionState.ESTABLISHED:
+ case ConnectionState.FETCHING:
+ case ConnectionState.ERROR:
+ case ConnectionState.OFFLINE:
+ return {
+ state: action.type,
+ message: action.message
+ }
+
+ default:
+ return state
+ }
+}
+
+export function startFetching() {
+ return { type: ConnectionState.FETCHING }
+}
+
+export function connectionEstablished() {
+ return { type: ConnectionState.ESTABLISHED }
+}
+
+export function connectionError(message) {
+ return { type: ConnectionState.ERROR, message }
+}
+export function setOffline() {
+ return { type: ConnectionState.OFFLINE }
+}
diff --git a/web/src/js/ducks/index.js b/web/src/js/ducks/index.js
index 753075fa..0f2426ec 100644
--- a/web/src/js/ducks/index.js
+++ b/web/src/js/ducks/index.js
@@ -1,12 +1,14 @@
-import { combineReducers } from 'redux'
-import eventLog from './eventLog'
-import flows from './flows'
-import settings from './settings'
-import ui from './ui/index'
+import { combineReducers } from "redux"
+import eventLog from "./eventLog"
+import flows from "./flows"
+import settings from "./settings"
+import ui from "./ui/index"
+import connection from "./connection"
export default combineReducers({
eventLog,
flows,
settings,
+ connection,
ui,
})
diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js
index ba604ea2..51ad4184 100644
--- a/web/src/js/ducks/ui/flow.js
+++ b/web/src/js/ducks/ui/flow.js
@@ -26,7 +26,7 @@ const defaultState = {
}
export default function reducer(state = defaultState, action) {
- let wasInEditMode = !!(state.modifiedFlow)
+ let wasInEditMode = state.modifiedFlow
let content = action.content || state.content
let isFullContentShown = content && content.length <= state.maxContentLines
@@ -89,14 +89,14 @@ export default function reducer(state = defaultState, action) {
...state,
tab: action.tab ? action.tab : 'request',
displayLarge: false,
- showFullContent: state.contentView == 'Edit'
+ showFullContent: state.contentView === 'Edit'
}
case SET_CONTENT_VIEW:
return {
...state,
contentView: action.contentView,
- showFullContent: action.contentView == 'Edit'
+ showFullContent: action.contentView === 'Edit'
}
case SET_CONTENT:
diff --git a/web/src/js/ducks/ui/header.js b/web/src/js/ducks/ui/header.js
index 6581149e..274d82aa 100644
--- a/web/src/js/ducks/ui/header.js
+++ b/web/src/js/ducks/ui/header.js
@@ -30,7 +30,7 @@ export default function reducer(state = defaultState, action) {
// Deselect
if (action.flowIds.length === 0 && state.isFlowSelected) {
let activeMenu = state.activeMenu
- if (activeMenu == 'Flow') {
+ if (activeMenu === 'Flow') {
activeMenu = 'Start'
}
return {
diff --git a/web/src/js/urlState.js b/web/src/js/urlState.js
index ca9187b2..7802bdb8 100644
--- a/web/src/js/urlState.js
+++ b/web/src/js/urlState.js
@@ -15,7 +15,7 @@ const Query = {
SHOW_EVENTLOG: "e"
};
-function updateStoreFromUrl(store) {
+export function updateStoreFromUrl(store) {
const [path, query] = window.location.hash.substr(1).split("?", 2)
const path_components = path.substr(1).split("/")
@@ -50,7 +50,7 @@ function updateStoreFromUrl(store) {
}
}
-function updateUrlFromStore(store) {
+export function updateUrlFromStore(store) {
const state = store.getState()
let query = {
[Query.SEARCH]: state.flows.filter,