diff options
Diffstat (limited to 'web/src/js')
-rw-r--r-- | web/src/js/__tests__/ducks/connectionSpec.js | 41 | ||||
-rw-r--r-- | web/src/js/__tests__/urlStateSpec.js | 100 | ||||
-rw-r--r-- | web/src/js/backends/websocket.js | 19 | ||||
-rw-r--r-- | web/src/js/components/Header.jsx | 6 | ||||
-rw-r--r-- | web/src/js/components/Header/ConnectionIndicator.jsx | 30 | ||||
-rw-r--r-- | web/src/js/components/common/DocsLink.jsx | 2 | ||||
-rw-r--r-- | web/src/js/ducks/connection.js | 44 | ||||
-rw-r--r-- | web/src/js/ducks/index.js | 12 | ||||
-rw-r--r-- | web/src/js/ducks/ui/flow.js | 6 | ||||
-rw-r--r-- | web/src/js/ducks/ui/header.js | 2 | ||||
-rw-r--r-- | web/src/js/urlState.js | 4 |
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, |