diff --git a/client/.eslintrc b/client/.eslintrc index 519fa33617..81022730e9 100644 --- a/client/.eslintrc +++ b/client/.eslintrc @@ -31,6 +31,7 @@ "no-param-reassign": 0, "no-restricted-properties": 0, "object-curly-spacing": 0, + "react/destructuring-assignment": 0, "react/jsx-closing-bracket-location": 0, "react/jsx-filename-extension": [ 2, diff --git a/client/.nvmrc b/client/.nvmrc index a244f6f35f..6fe1005059 100644 --- a/client/.nvmrc +++ b/client/.nvmrc @@ -1 +1 @@ -v8.9.0 +v8.10.0 diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 0587ca8b6f..ec5bb0acc7 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -1,42 +1,19 @@ -import debug from 'debug'; -import { fromJS } from 'immutable'; - import ActionTypes from '../constants/action-types'; import { saveGraph } from '../utils/file-utils'; import { clearStoredViewState, updateRoute } from '../utils/router-utils'; -import { - doControlRequest, - getAllNodes, - getResourceViewNodesSnapshot, - getNodeDetails, - getTopologies, - deletePipe, - stopPolling, - teardownWebsockets, - getNodes, -} from '../utils/web-api-utils'; import { isPausedSelector } from '../selectors/time-travel'; import { - availableMetricTypesSelector, nextPinnedMetricTypeSelector, previousPinnedMetricTypeSelector, - pinnedMetricSelector, } from '../selectors/node-metric'; -import { - isResourceViewModeSelector, - resourceViewAvailableSelector, -} from '../selectors/topology'; +import { isResourceViewModeSelector } from '../selectors/topology'; import { GRAPH_VIEW_MODE, TABLE_VIEW_MODE, - RESOURCE_VIEW_MODE, } from '../constants/naming'; -const log = debug('scope:app-actions'); - - export function showHelp() { return { type: ActionTypes.SHOW_HELP }; } @@ -181,41 +158,10 @@ export function updateSearch(searchQuery = '', pinnedSearches = []) { }; } -export function focusSearch() { - return (dispatch, getState) => { - dispatch({ type: ActionTypes.FOCUS_SEARCH }); - // update nodes cache to allow search across all topologies, - // wait a second until animation is over - // NOTE: This will cause matching recalculation (and rerendering) - // of all the nodes in the topology, instead applying it only on - // the nodes delta. The solution would be to implement deeper - // search selectors with per-node caching instead of per-topology. - setTimeout(() => { - getAllNodes(getState(), dispatch); - }, 1200); - }; -} - export function blurSearch() { return { type: ActionTypes.BLUR_SEARCH }; } -export function changeTopologyOption(option, value, topologyId, addOrRemove) { - return (dispatch, getState) => { - dispatch({ - addOrRemove, - option, - topologyId, - type: ActionTypes.CHANGE_TOPOLOGY_OPTION, - value - }); - updateRoute(getState); - // update all request workers with new options - getTopologies(getState, dispatch); - getNodes(getState, dispatch); - }; -} - export function clickBackground() { return (dispatch, getState) => { dispatch({ @@ -225,18 +171,6 @@ export function clickBackground() { }; } -export function clickCloseDetails(nodeId) { - return (dispatch, getState) => { - dispatch({ - nodeId, - type: ActionTypes.CLICK_CLOSE_DETAILS - }); - // Pull the most recent details for the next details panel that comes into focus. - getNodeDetails(getState, dispatch); - updateRoute(getState); - }; -} - export function closeTerminal(pipeId) { return (dispatch, getState) => { dispatch({ @@ -297,102 +231,6 @@ export function setTableView() { }; } -export function setResourceView() { - return (dispatch, getState) => { - if (resourceViewAvailableSelector(getState())) { - dispatch({ - type: ActionTypes.SET_VIEW_MODE, - viewMode: RESOURCE_VIEW_MODE, - }); - // Pin the first metric if none of the visible ones is pinned. - const state = getState(); - if (!pinnedMetricSelector(state)) { - const firstAvailableMetricType = availableMetricTypesSelector(state).first(); - dispatch(pinMetric(firstAvailableMetricType)); - } - getResourceViewNodesSnapshot(getState(), dispatch); - updateRoute(getState); - } - }; -} - -export function clickNode(nodeId, label, origin, topologyId = null) { - return (dispatch, getState) => { - dispatch({ - label, - nodeId, - origin, - topologyId, - type: ActionTypes.CLICK_NODE, - }); - updateRoute(getState); - getNodeDetails(getState, dispatch); - }; -} - -export function pauseTimeAtNow() { - return (dispatch, getState) => { - dispatch({ - type: ActionTypes.PAUSE_TIME_AT_NOW - }); - updateRoute(getState); - if (!getState().get('nodesLoaded')) { - getNodes(getState, dispatch); - if (isResourceViewModeSelector(getState())) { - getResourceViewNodesSnapshot(getState(), dispatch); - } - } - }; -} - -export function clickRelative(nodeId, topologyId, label, origin) { - return (dispatch, getState) => { - dispatch({ - label, - nodeId, - origin, - topologyId, - type: ActionTypes.CLICK_RELATIVE - }); - updateRoute(getState); - getNodeDetails(getState, dispatch); - }; -} - -function updateTopology(dispatch, getState) { - const state = getState(); - // If we're in the resource view, get the snapshot of all the relevant node topologies. - if (isResourceViewModeSelector(state)) { - getResourceViewNodesSnapshot(state, dispatch); - } - updateRoute(getState); - // NOTE: This is currently not needed for our static resource - // view, but we'll need it here later and it's simpler to just - // keep it than to redo the nodes delta updating logic. - getNodes(getState, dispatch); -} - -export function clickShowTopologyForNode(topologyId, nodeId) { - return (dispatch, getState) => { - dispatch({ - nodeId, - topologyId, - type: ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE - }); - updateTopology(dispatch, getState); - }; -} - -export function clickTopology(topologyId) { - return (dispatch, getState) => { - dispatch({ - topologyId, - type: ActionTypes.CLICK_TOPOLOGY - }); - updateTopology(dispatch, getState); - }; -} - export function cacheZoomState(zoomState) { return { type: ActionTypes.CACHE_ZOOM_STATE, @@ -420,17 +258,6 @@ export function closeWebsocket() { }; } -export function doControl(nodeId, control) { - return (dispatch) => { - dispatch({ - control, - nodeId, - type: ActionTypes.DO_CONTROL - }); - doControlRequest(nodeId, control, dispatch); - }; -} - export function enterEdge(edgeId) { return { edgeId, @@ -528,23 +355,6 @@ export function receiveNodesDelta(delta) { }; } -export function resumeTime() { - return (dispatch, getState) => { - if (isPausedSelector(getState())) { - dispatch({ - type: ActionTypes.RESUME_TIME - }); - updateRoute(getState); - // After unpausing, all of the following calls will re-activate polling. - getTopologies(getState, dispatch); - getNodes(getState, dispatch, true); - if (isResourceViewModeSelector(getState())) { - getResourceViewNodesSnapshot(getState(), dispatch); - } - } - }; -} - export function receiveNodes(nodes) { return { nodes, @@ -552,26 +362,6 @@ export function receiveNodes(nodes) { }; } -export function jumpToTime(timestamp) { - return (dispatch, getState) => { - dispatch({ - timestamp, - type: ActionTypes.JUMP_TO_TIME, - }); - updateRoute(getState); - getTopologies(getState, dispatch); - if (!getState().get('nodesLoaded')) { - getNodes(getState, dispatch); - if (isResourceViewModeSelector(getState())) { - getResourceViewNodesSnapshot(getState(), dispatch); - } - } else { - // Get most recent details before freezing the state. - getNodeDetails(getState, dispatch); - } - }; -} - export function receiveNodesForTopology(nodes, topologyId) { return { nodes, @@ -580,53 +370,6 @@ export function receiveNodesForTopology(nodes, topologyId) { }; } -export function receiveTopologies(topologies) { - return (dispatch, getState) => { - const firstLoad = !getState().get('topologiesLoaded'); - dispatch({ - topologies, - type: ActionTypes.RECEIVE_TOPOLOGIES - }); - getNodes(getState, dispatch); - // Populate search matches on first load - const state = getState(); - // Fetch all the relevant nodes once on first load - if (firstLoad && isResourceViewModeSelector(state)) { - getResourceViewNodesSnapshot(state, dispatch); - } - }; -} - -export function receiveApiDetails(apiDetails) { - return (dispatch, getState) => { - const isFirstTime = !getState().get('version'); - const pausedAt = getState().get('pausedAt'); - - dispatch({ - capabilities: fromJS(apiDetails.capabilities || {}), - hostname: apiDetails.hostname, - newVersion: apiDetails.newVersion, - plugins: apiDetails.plugins, - type: ActionTypes.RECEIVE_API_DETAILS, - version: apiDetails.version, - }); - - // On initial load either start time travelling at the pausedAt timestamp - // (if it was given as URL param) if time travelling is enabled, otherwise - // simply pause at the present time which is arguably the next best thing - // we could do. - // NOTE: We can't make this decision before API details are received because - // we have no prior info on whether time travel would be available. - if (isFirstTime && pausedAt) { - if (apiDetails.capabilities && apiDetails.capabilities.historic_reports) { - dispatch(jumpToTime(pausedAt)); - } else { - dispatch(pauseTimeAtNow()); - } - } - }; -} - export function receiveControlNodeRemoved(nodeId) { return (dispatch, getState) => { dispatch({ @@ -647,34 +390,6 @@ export function receiveControlPipeFromParams(pipeId, rawTty, resizeTtyControl) { }; } -export function receiveControlPipe(pipeId, nodeId, rawTty, resizeTtyControl, control) { - return (dispatch, getState) => { - const state = getState(); - if (state.get('nodeDetails').last() - && nodeId !== state.get('nodeDetails').last().id) { - log('Node was deselected before we could set up control!'); - deletePipe(pipeId, dispatch); - return; - } - - const controlPipe = state.get('controlPipes').last(); - if (controlPipe && controlPipe.get('id') !== pipeId) { - deletePipe(controlPipe.get('id'), dispatch); - } - - dispatch({ - control, - nodeId, - pipeId, - rawTty, - resizeTtyControl, - type: ActionTypes.RECEIVE_CONTROL_PIPE - }); - - updateRoute(getState); - }; -} - export function receiveControlPipeStatus(pipeId, status) { return { pipeId, @@ -708,44 +423,9 @@ export function setContrastMode(enabled) { }; } -export function getTopologiesWithInitialPoll() { - return (dispatch, getState) => { - getTopologies(getState, dispatch, true); - }; -} - -export function route(urlState) { - return (dispatch, getState) => { - dispatch({ - state: urlState, - type: ActionTypes.ROUTE_TOPOLOGY - }); - // Handle Time Travel state update through separate actions as it's more complex. - // This is mostly to handle switching contexts Explore <-> Monitor in WC while - // the timestamp keeps changing - e.g. if we were Time Travelling in Scope and - // then went live in Monitor, switching back to Explore should properly close - // the Time Travel etc, not just update the pausedAt state directly. - if (!urlState.pausedAt) { - dispatch(resumeTime()); - } else { - dispatch(jumpToTime(urlState.pausedAt)); - } - // update all request workers with new options - getTopologies(getState, dispatch); - getNodes(getState, dispatch); - // If we are landing on the resource view page, we need to fetch not only all the - // nodes for the current topology, but also the nodes of all the topologies that make - // the layers in the resource view. - const state = getState(); - if (isResourceViewModeSelector(state)) { - getResourceViewNodesSnapshot(state, dispatch); - } - }; -} - export function resetLocalViewState() { return (dispatch) => { - dispatch({type: ActionTypes.RESET_LOCAL_VIEW_STATE}); + dispatch({ type: ActionTypes.RESET_LOCAL_VIEW_STATE }); clearStoredViewState(); // eslint-disable-next-line prefer-destructuring window.location.href = window.location.href.split('#')[0]; @@ -768,16 +448,6 @@ export function changeInstance() { }; } -export function shutdown() { - return (dispatch) => { - stopPolling(); - teardownWebsockets(); - dispatch({ - type: ActionTypes.SHUTDOWN - }); - }; -} - export function setMonitorState(monitor) { return { monitor, diff --git a/client/app/scripts/actions/request-actions.js b/client/app/scripts/actions/request-actions.js new file mode 100644 index 0000000000..82a7e7d9fd --- /dev/null +++ b/client/app/scripts/actions/request-actions.js @@ -0,0 +1,608 @@ +/* + +This file consists of functions that both dispatch actions to Redux and also make API requests. + +TODO: Refactor all the methods below so that the split between actions and +requests is more clear, and make user components make explicit calls to requests +and dispatch actions when handling request promises. + +*/ +import debug from 'debug'; +import { fromJS } from 'immutable'; + +import ActionTypes from '../constants/action-types'; +import { RESOURCE_VIEW_MODE } from '../constants/naming'; +import { + API_REFRESH_INTERVAL, + TOPOLOGY_REFRESH_INTERVAL, +} from '../constants/timer'; +import { updateRoute } from '../utils/router-utils'; +import { getCurrentTopologyUrl } from '../utils/topology-utils'; +import { + doRequest, + getApiPath, + getAllNodes, + getNodesOnce, + deletePipe, + getNodeDetails, + getResourceViewNodesSnapshot, + topologiesUrl, + buildWebsocketUrl, +} from '../utils/web-api-utils'; +import { + availableMetricTypesSelector, + pinnedMetricSelector, +} from '../selectors/node-metric'; +import { + isResourceViewModeSelector, + resourceViewAvailableSelector, + activeTopologyOptionsSelector, +} from '../selectors/topology'; +import { isPausedSelector } from '../selectors/time-travel'; + +import { + receiveControlNodeRemoved, + receiveControlPipeStatus, + receiveControlSuccess, + receiveControlError, + receiveError, + pinMetric, + openWebsocket, + closeWebsocket, + receiveNodesDelta, + clearControlError, + blurSearch, +} from './app-actions'; + + +const log = debug('scope:app-actions'); +const reconnectTimerInterval = 5000; +const FIRST_RENDER_TOO_LONG_THRESHOLD = 100; // ms + +let socket; +let topologyTimer = 0; +let controlErrorTimer = 0; +let reconnectTimer = 0; +let apiDetailsTimer = 0; +let continuePolling = true; +let firstMessageOnWebsocketAt = null; +let createWebsocketAt = null; +let currentUrl = null; + +function createWebsocket(websocketUrl, getState, dispatch) { + if (socket) { + socket.onclose = null; + socket.onerror = null; + socket.close(); + // onclose() is not called, but that's fine since we're opening a new one + // right away + } + + // profiling + createWebsocketAt = new Date(); + firstMessageOnWebsocketAt = null; + + socket = new WebSocket(websocketUrl); + + socket.onopen = () => { + log(`Opening websocket to ${websocketUrl}`); + dispatch(openWebsocket()); + }; + + socket.onclose = () => { + clearTimeout(reconnectTimer); + log(`Closing websocket to ${websocketUrl}`, socket.readyState); + socket = null; + dispatch(closeWebsocket()); + + if (continuePolling && !isPausedSelector(getState())) { + reconnectTimer = setTimeout(() => { + createWebsocket(websocketUrl, getState, dispatch); + }, reconnectTimerInterval); + } + }; + + socket.onerror = () => { + log(`Error in websocket to ${websocketUrl}`); + dispatch(receiveError(websocketUrl)); + }; + + socket.onmessage = (event) => { + const msg = JSON.parse(event.data); + dispatch(receiveNodesDelta(msg)); + + // profiling (receiveNodesDelta triggers synchronous render) + if (!firstMessageOnWebsocketAt) { + firstMessageOnWebsocketAt = new Date(); + const timeToFirstMessage = firstMessageOnWebsocketAt - createWebsocketAt; + if (timeToFirstMessage > FIRST_RENDER_TOO_LONG_THRESHOLD) { + log( + 'Time (ms) to first nodes render after websocket was created', + firstMessageOnWebsocketAt - createWebsocketAt + ); + } + } + }; +} + +function teardownWebsockets() { + clearTimeout(reconnectTimer); + if (socket) { + socket.onerror = null; + socket.onclose = null; + socket.onmessage = null; + socket.onopen = null; + socket.close(); + socket = null; + currentUrl = null; + } +} + +function updateWebsocketChannel(getState, dispatch, forceRequest) { + const topologyUrl = getCurrentTopologyUrl(getState()); + const topologyOptions = activeTopologyOptionsSelector(getState()); + const websocketUrl = buildWebsocketUrl(topologyUrl, topologyOptions, getState()); + // Only recreate websocket if url changed or if forced (weave cloud instance reload); + const isNewUrl = websocketUrl !== currentUrl; + // `topologyUrl` can be undefined initially, so only create a socket if it is truthy + // and no socket exists, or if we get a new url. + if (topologyUrl && (!socket || isNewUrl || forceRequest)) { + createWebsocket(websocketUrl, getState, dispatch); + currentUrl = websocketUrl; + } +} + +function getNodes(getState, dispatch, forceRequest = false) { + if (isPausedSelector(getState())) { + getNodesOnce(getState, dispatch); + } else { + updateWebsocketChannel(getState, dispatch, forceRequest); + } + getNodeDetails(getState, dispatch); +} + +export function pauseTimeAtNow() { + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.PAUSE_TIME_AT_NOW + }); + updateRoute(getState); + if (!getState().get('nodesLoaded')) { + getNodes(getState, dispatch); + if (isResourceViewModeSelector(getState())) { + getResourceViewNodesSnapshot(getState(), dispatch); + } + } + }; +} + +function receiveTopologies(topologies) { + return (dispatch, getState) => { + const firstLoad = !getState().get('topologiesLoaded'); + dispatch({ + topologies, + type: ActionTypes.RECEIVE_TOPOLOGIES + }); + getNodes(getState, dispatch); + // Populate search matches on first load + const state = getState(); + // Fetch all the relevant nodes once on first load + if (firstLoad && isResourceViewModeSelector(state)) { + getResourceViewNodesSnapshot(state, dispatch); + } + }; +} + +function getTopologiesOnce(getState, dispatch) { + const url = topologiesUrl(getState()); + doRequest({ + error: (req) => { + log(`Error in topology request: ${req.responseText}`); + dispatch(receiveError(url)); + }, + success: (res) => { + dispatch(receiveTopologies(res)); + }, + url + }); +} + +function pollTopologies(getState, dispatch, initialPoll = false) { + // Used to resume polling when navigating between pages in Weave Cloud. + continuePolling = initialPoll === true ? true : continuePolling; + clearTimeout(topologyTimer); + // NOTE: getState is called every time to make sure the up-to-date state is used. + const url = topologiesUrl(getState()); + doRequest({ + error: (req) => { + log(`Error in topology request: ${req.responseText}`); + dispatch(receiveError(url)); + // Only retry in stand-alone mode + if (continuePolling && !isPausedSelector(getState())) { + topologyTimer = setTimeout(() => { + pollTopologies(getState, dispatch); + }, TOPOLOGY_REFRESH_INTERVAL); + } + }, + success: (res) => { + if (continuePolling && !isPausedSelector(getState())) { + dispatch(receiveTopologies(res)); + topologyTimer = setTimeout(() => { + pollTopologies(getState, dispatch); + }, TOPOLOGY_REFRESH_INTERVAL); + } + }, + url + }); +} + +function getTopologies(getState, dispatch, forceRequest) { + if (isPausedSelector(getState())) { + getTopologiesOnce(getState, dispatch); + } else { + pollTopologies(getState, dispatch, forceRequest); + } +} + +export function jumpToTime(timestamp) { + return (dispatch, getState) => { + dispatch({ + timestamp, + type: ActionTypes.JUMP_TO_TIME, + }); + updateRoute(getState); + getTopologies(getState, dispatch); + if (!getState().get('nodesLoaded')) { + getNodes(getState, dispatch); + if (isResourceViewModeSelector(getState())) { + getResourceViewNodesSnapshot(getState(), dispatch); + } + } else { + // Get most recent details before freezing the state. + getNodeDetails(getState, dispatch); + } + }; +} + +export function receiveApiDetails(apiDetails) { + return (dispatch, getState) => { + const isFirstTime = !getState().get('version'); + const pausedAt = getState().get('pausedAt'); + + dispatch({ + capabilities: fromJS(apiDetails.capabilities || {}), + hostname: apiDetails.hostname, + newVersion: apiDetails.newVersion, + plugins: apiDetails.plugins, + type: ActionTypes.RECEIVE_API_DETAILS, + version: apiDetails.version, + }); + + // On initial load either start time travelling at the pausedAt timestamp + // (if it was given as URL param) if time travelling is enabled, otherwise + // simply pause at the present time which is arguably the next best thing + // we could do. + // NOTE: We can't make this decision before API details are received because + // we have no prior info on whether time travel would be available. + if (isFirstTime && pausedAt) { + if (apiDetails.capabilities && apiDetails.capabilities.historic_reports) { + dispatch(jumpToTime(pausedAt)); + } else { + dispatch(pauseTimeAtNow()); + } + } + }; +} + +export function getApiDetails(dispatch) { + clearTimeout(apiDetailsTimer); + const url = `${getApiPath()}/api`; + doRequest({ + error: (req) => { + log(`Error in api details request: ${req.responseText}`); + receiveError(url); + if (continuePolling) { + apiDetailsTimer = setTimeout(() => { + getApiDetails(dispatch); + }, API_REFRESH_INTERVAL / 2); + } + }, + success: (res) => { + dispatch(receiveApiDetails(res)); + if (continuePolling) { + apiDetailsTimer = setTimeout(() => { + getApiDetails(dispatch); + }, API_REFRESH_INTERVAL); + } + }, + url + }); +} + +function stopPolling() { + clearTimeout(apiDetailsTimer); + clearTimeout(topologyTimer); + continuePolling = false; +} + +export function focusSearch() { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.FOCUS_SEARCH }); + // update nodes cache to allow search across all topologies, + // wait a second until animation is over + // NOTE: This will cause matching recalculation (and rerendering) + // of all the nodes in the topology, instead applying it only on + // the nodes delta. The solution would be to implement deeper + // search selectors with per-node caching instead of per-topology. + setTimeout(() => { + getAllNodes(getState(), dispatch); + }, 1200); + }; +} + +export function getPipeStatus(pipeId, dispatch) { + const url = `${getApiPath()}/api/pipe/${encodeURIComponent(pipeId)}/check`; + doRequest({ + complete: (res) => { + const status = { + 204: 'PIPE_ALIVE', + 404: 'PIPE_DELETED' + }[res.status]; + + if (!status) { + log('Unexpected pipe status:', res.status); + return; + } + + dispatch(receiveControlPipeStatus(pipeId, status)); + }, + method: 'GET', + url + }); +} + +export function receiveControlPipe(pipeId, nodeId, rawTty, resizeTtyControl, control) { + return (dispatch, getState) => { + const state = getState(); + if (state.get('nodeDetails').last() + && nodeId !== state.get('nodeDetails').last().id) { + log('Node was deselected before we could set up control!'); + deletePipe(pipeId, dispatch); + return; + } + + const controlPipe = state.get('controlPipes').last(); + if (controlPipe && controlPipe.get('id') !== pipeId) { + deletePipe(controlPipe.get('id'), dispatch); + } + + dispatch({ + control, + nodeId, + pipeId, + rawTty, + resizeTtyControl, + type: ActionTypes.RECEIVE_CONTROL_PIPE + }); + + updateRoute(getState); + }; +} + +function doControlRequest(nodeId, control, dispatch) { + clearTimeout(controlErrorTimer); + const url = `${getApiPath()}/api/control/${encodeURIComponent(control.probeId)}/` + + `${encodeURIComponent(control.nodeId)}/${control.id}`; + doRequest({ + error: (err) => { + dispatch(receiveControlError(nodeId, err.response)); + controlErrorTimer = setTimeout(() => { + dispatch(clearControlError(nodeId)); + }, 10000); + }, + method: 'POST', + success: (res) => { + dispatch(receiveControlSuccess(nodeId)); + if (res) { + if (res.pipe) { + dispatch(blurSearch()); + const resizeTtyControl = res.resize_tty_control + && { id: res.resize_tty_control, nodeId: control.nodeId, probeId: control.probeId }; + dispatch(receiveControlPipe( + res.pipe, + nodeId, + res.raw_tty, + resizeTtyControl, + control + )); + } + if (res.removedNode) { + dispatch(receiveControlNodeRemoved(nodeId)); + } + } + }, + url + }); +} + +export function doControl(nodeId, control) { + return (dispatch) => { + dispatch({ + control, + nodeId, + type: ActionTypes.DO_CONTROL + }); + doControlRequest(nodeId, control, dispatch); + }; +} + +export function shutdown() { + return (dispatch) => { + stopPolling(); + teardownWebsockets(); + dispatch({ + type: ActionTypes.SHUTDOWN + }); + }; +} + +export function setResourceView() { + return (dispatch, getState) => { + if (resourceViewAvailableSelector(getState())) { + dispatch({ + type: ActionTypes.SET_VIEW_MODE, + viewMode: RESOURCE_VIEW_MODE, + }); + // Pin the first metric if none of the visible ones is pinned. + const state = getState(); + if (!pinnedMetricSelector(state)) { + const firstAvailableMetricType = availableMetricTypesSelector(state).first(); + dispatch(pinMetric(firstAvailableMetricType)); + } + getResourceViewNodesSnapshot(getState(), dispatch); + updateRoute(getState); + } + }; +} + +export function changeTopologyOption(option, value, topologyId, addOrRemove) { + return (dispatch, getState) => { + dispatch({ + addOrRemove, + option, + topologyId, + type: ActionTypes.CHANGE_TOPOLOGY_OPTION, + value + }); + updateRoute(getState); + // update all request workers with new options + getTopologies(getState, dispatch); + getNodes(getState, dispatch); + }; +} + +export function getTopologiesWithInitialPoll() { + return (dispatch, getState) => { + getTopologies(getState, dispatch, true); + }; +} + +export function resumeTime() { + return (dispatch, getState) => { + if (isPausedSelector(getState())) { + dispatch({ + type: ActionTypes.RESUME_TIME + }); + updateRoute(getState); + // After unpausing, all of the following calls will re-activate polling. + getTopologies(getState, dispatch); + getNodes(getState, dispatch, true); + if (isResourceViewModeSelector(getState())) { + getResourceViewNodesSnapshot(getState(), dispatch); + } + } + }; +} + +export function route(urlState) { + return (dispatch, getState) => { + dispatch({ + state: urlState, + type: ActionTypes.ROUTE_TOPOLOGY + }); + // Handle Time Travel state update through separate actions as it's more complex. + // This is mostly to handle switching contexts Explore <-> Monitor in WC while + // the timestamp keeps changing - e.g. if we were Time Travelling in Scope and + // then went live in Monitor, switching back to Explore should properly close + // the Time Travel etc, not just update the pausedAt state directly. + if (!urlState.pausedAt) { + dispatch(resumeTime()); + } else { + dispatch(jumpToTime(urlState.pausedAt)); + } + // update all request workers with new options + getTopologies(getState, dispatch); + getNodes(getState, dispatch); + // If we are landing on the resource view page, we need to fetch not only all the + // nodes for the current topology, but also the nodes of all the topologies that make + // the layers in the resource view. + const state = getState(); + if (isResourceViewModeSelector(state)) { + getResourceViewNodesSnapshot(state, dispatch); + } + }; +} + +export function clickCloseDetails(nodeId) { + return (dispatch, getState) => { + dispatch({ + nodeId, + type: ActionTypes.CLICK_CLOSE_DETAILS + }); + // Pull the most recent details for the next details panel that comes into focus. + getNodeDetails(getState, dispatch); + updateRoute(getState); + }; +} + +export function clickNode(nodeId, label, origin, topologyId = null) { + return (dispatch, getState) => { + dispatch({ + label, + nodeId, + origin, + topologyId, + type: ActionTypes.CLICK_NODE, + }); + updateRoute(getState); + getNodeDetails(getState, dispatch); + }; +} + +export function clickRelative(nodeId, topologyId, label, origin) { + return (dispatch, getState) => { + dispatch({ + label, + nodeId, + origin, + topologyId, + type: ActionTypes.CLICK_RELATIVE + }); + updateRoute(getState); + getNodeDetails(getState, dispatch); + }; +} + +function updateTopology(dispatch, getState) { + const state = getState(); + // If we're in the resource view, get the snapshot of all the relevant node topologies. + if (isResourceViewModeSelector(state)) { + getResourceViewNodesSnapshot(state, dispatch); + } + updateRoute(getState); + // NOTE: This is currently not needed for our static resource + // view, but we'll need it here later and it's simpler to just + // keep it than to redo the nodes delta updating logic. + getNodes(getState, dispatch); +} + +export function clickShowTopologyForNode(topologyId, nodeId) { + return (dispatch, getState) => { + dispatch({ + nodeId, + topologyId, + type: ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE + }); + updateTopology(dispatch, getState); + }; +} + +export function clickTopology(topologyId) { + return (dispatch, getState) => { + dispatch({ + topologyId, + type: ActionTypes.CLICK_TOPOLOGY + }); + updateTopology(dispatch, getState); + }; +} diff --git a/client/app/scripts/charts/edge-container.js b/client/app/scripts/charts/edge-container.js index 2ad0f81999..9d357d9459 100644 --- a/client/app/scripts/charts/edge-container.js +++ b/client/app/scripts/charts/edge-container.js @@ -88,12 +88,11 @@ export default class EdgeContainer extends React.PureComponent { ...waypointsMap.toJS(), }}> { - ({ interpolatedThickness, ...interpolatedWaypoints }) => - transformedEdge( - forwardedProps, - waypointsMapToArray(fromJS(interpolatedWaypoints)), - interpolatedThickness - ) + ({ interpolatedThickness, ...interpolatedWaypoints }) => transformedEdge( + forwardedProps, + waypointsMapToArray(fromJS(interpolatedWaypoints)), + interpolatedThickness + ) } ); diff --git a/client/app/scripts/charts/node-container.js b/client/app/scripts/charts/node-container.js index 636412465f..c7dd158401 100644 --- a/client/app/scripts/charts/node-container.js +++ b/client/app/scripts/charts/node-container.js @@ -7,7 +7,8 @@ import { getMetricValue, getMetricColor, } from '../utils/metric-utils'; -import { clickNode, enterNode, leaveNode } from '../actions/app-actions'; +import { clickNode } from '../actions/request-actions'; +import { enterNode, leaveNode } from '../actions/app-actions'; import { trackAnalyticsEvent } from '../utils/tracking-utils'; import { getNodeColor } from '../utils/color-utils'; import MatchedResults from '../components/matched-results'; diff --git a/client/app/scripts/charts/nodes-chart-elements.js b/client/app/scripts/charts/nodes-chart-elements.js index 52c3e3b1fc..741fbb9d4e 100644 --- a/client/app/scripts/charts/nodes-chart-elements.js +++ b/client/app/scripts/charts/nodes-chart-elements.js @@ -63,9 +63,9 @@ class NodesChartElements extends React.Component { nodeDisplayLayer(node) { if (node.get('id') === this.props.mouseOverNodeId) { return HOVERED_NODES_LAYER; - } else if (node.get('blurred') && !node.get('focused')) { + } if (node.get('blurred') && !node.get('focused')) { return BLURRED_NODES_LAYER; - } else if (node.get('highlighted')) { + } if (node.get('highlighted')) { return HIGHLIGHTED_NODES_LAYER; } return NORMAL_NODES_LAYER; @@ -74,9 +74,9 @@ class NodesChartElements extends React.Component { edgeDisplayLayer(edge) { if (edge.get('id') === this.props.mouseOverEdgeId) { return HOVERED_EDGES_LAYER; - } else if (edge.get('blurred') && !edge.get('focused')) { + } if (edge.get('blurred') && !edge.get('focused')) { return BLURRED_EDGES_LAYER; - } else if (edge.get('highlighted')) { + } if (edge.get('highlighted')) { return HIGHLIGHTED_EDGES_LAYER; } return NORMAL_EDGES_LAYER; @@ -140,8 +140,8 @@ class NodesChartElements extends React.Component { const sourceInNetwork = selectedNetworkNodesIds.contains(edge.get('source')); const targetInNetwork = selectedNetworkNodesIds.contains(edge.get('target')); const notInNetwork = this.props.selectedNetwork && (!sourceInNetwork || !targetInNetwork); - return edge.set('blurred', !edge.get('highlighted') && !edge.get('focused') && - (otherNodesSelected || notMatched || notInNetwork)); + return edge.set('blurred', !edge.get('highlighted') && !edge.get('focused') + && (otherNodesSelected || notMatched || notInNetwork)); } edgeScaleDecorator(edge) { diff --git a/client/app/scripts/charts/nodes-grid.js b/client/app/scripts/charts/nodes-grid.js index ffaa8126a9..8cd2c6d6cf 100644 --- a/client/app/scripts/charts/nodes-grid.js +++ b/client/app/scripts/charts/nodes-grid.js @@ -1,4 +1,4 @@ -/* eslint react/jsx-no-bind: "off", no-multi-comp: "off" */ +/* eslint react/jsx-no-bind: "off" */ import React from 'react'; import styled from 'styled-components'; import { connect } from 'react-redux'; @@ -6,7 +6,8 @@ import { List as makeList, Map as makeMap } from 'immutable'; import capitalize from 'lodash/capitalize'; import NodeDetailsTable from '../components/node-details/node-details-table'; -import { clickNode, sortOrderChanged } from '../actions/app-actions'; +import { clickNode } from '../actions/request-actions'; +import { sortOrderChanged } from '../actions/app-actions'; import { shownNodesSelector } from '../selectors/node-filters'; import { trackAnalyticsEvent } from '../utils/tracking-utils'; import { findTopologyById } from '../utils/topology-utils'; @@ -55,7 +56,11 @@ function getColumns(nodes, topologies) { .toList() .flatMap((n) => { const metadata = (n.get('metadata') || makeList()) - .map(m => makeMap({ dataType: m.get('dataType'), id: m.get('id'), label: m.get('label') })); + .map(m => makeMap({ + dataType: m.get('dataType'), + id: m.get('id'), + label: m.get('label') + })); return metadata; }) .toSet() @@ -68,7 +73,10 @@ function getColumns(nodes, topologies) { .toList() .flatMap((n) => { const metadata = (n.get('parents') || makeList()) - .map(m => makeMap({ id: m.get('topologyId'), label: topologyLabel(topologies, m.get('topologyId')) })); + .map(m => makeMap({ + id: m.get('topologyId'), + label: topologyLabel(topologies, m.get('topologyId')) + })); return metadata; }) .toSet() @@ -91,7 +99,9 @@ function renderIdCell({
- {label} {showSubLabel && {labelMinor}} + {label} + {' '} + {showSubLabel && {labelMinor}}
); @@ -126,8 +136,9 @@ class NodesGrid extends React.Component { const { nodes, gridSortedBy, gridSortedDesc, searchNodeMatches, searchQuery, windowHeight, topologies } = this.props; - const height = - this.tableRef ? windowHeight - this.tableRef.getBoundingClientRect().top - 30 : 0; + const height = this.tableRef + ? windowHeight - this.tableRef.getBoundingClientRect().top - 30 + : 0; const cmpStyle = { height, paddingLeft: 40, @@ -152,20 +163,22 @@ class NodesGrid extends React.Component { return (
- {nodes.size > 0 && } + {nodes.size > 0 && ( + + )}
); } diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js index 2d0d7321cd..cf7ed85bdd 100644 --- a/client/app/scripts/charts/nodes-layout.js +++ b/client/app/scripts/charts/nodes-layout.js @@ -408,7 +408,7 @@ function copyLayoutProperties(layout, nodeCache, edgeCache) { if (edgeCache.has(edge.get('id')) && hasSameEndpoints(edgeCache.get(edge.get('id')), result.nodes)) { return edge.merge(edgeCache.get(edge.get('id'))); - } else if (nodeCache.get(edge.get('source')) && nodeCache.get(edge.get('target'))) { + } if (nodeCache.get(edge.get('source')) && nodeCache.get(edge.get('target'))) { return setSimpleEdgePoints(edge, nodeCache); } return edge; diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 0b616cfc60..b9a4ebc741 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -18,9 +18,7 @@ import Status from './status'; import Topologies from './topologies'; import TopologyOptions from './topology-options'; import Overlay from './overlay'; -import { getApiDetails } from '../utils/web-api-utils'; import { - focusSearch, pinNextMetric, pinPreviousMetric, hitEsc, @@ -29,12 +27,16 @@ import { setGraphView, setMonitorState, setTableView, - setResourceView, setStoreViewState, - shutdown, setViewportDimensions, - getTopologiesWithInitialPoll, } from '../actions/app-actions'; +import { + focusSearch, + getApiDetails, + setResourceView, + getTopologiesWithInitialPoll, + shutdown, +} from '../actions/request-actions'; import Details from './details'; import Nodes from './nodes'; import TimeControl from './time-control'; @@ -42,7 +44,8 @@ import TimeTravelWrapper from './time-travel-wrapper'; import ViewModeSelector from './view-mode-selector'; import NetworkSelector from './networks-selector'; import DebugToolbar, { showingDebugToolbar, toggleDebugToolbar } from './debug-toolbar'; -import { getRouter, getUrlState } from '../utils/router-utils'; +import { getUrlState } from '../utils/router-utils'; +import { getRouter } from '../router'; import { trackAnalyticsEvent } from '../utils/tracking-utils'; import { availableNetworksSelector } from '../selectors/node-networks'; import { timeTravelSupportedSelector } from '../selectors/time-travel'; @@ -210,19 +213,23 @@ class App extends React.Component { {showingTroubleshootingMenu && } - {showingDetails &&
} + /> + )}
{timeTravelSupported && this.props.renderTimeTravel()}
- {!isIframe && + {!isIframe + && ( + ) }
diff --git a/client/app/scripts/components/cloud-feature.js b/client/app/scripts/components/cloud-feature.js index 0708a37eff..440f745117 100644 --- a/client/app/scripts/components/cloud-feature.js +++ b/client/app/scripts/components/cloud-feature.js @@ -26,6 +26,8 @@ class CloudFeature extends React.Component { } } +/* eslint-disable react/forbid-prop-types */ +// TODO: Remove this component as part of https://github.com/weaveworks/scope/issues/3278. CloudFeature.contextTypes = { router: PropTypes.object, serviceStore: PropTypes.object, @@ -36,5 +38,6 @@ CloudFeature.childContextTypes = { router: PropTypes.object, store: PropTypes.object }; +/* eslint-enable react/forbid-prop-types */ export default connect()(CloudFeature); diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js index fa71ffb802..bb84c609df 100644 --- a/client/app/scripts/components/debug-toolbar.js +++ b/client/app/scripts/components/debug-toolbar.js @@ -1,7 +1,9 @@ /* eslint react/jsx-no-bind: "off" */ import React from 'react'; import { connect } from 'react-redux'; -import { sampleSize, sample, random, range, flattenDeep, times } from 'lodash'; +import { + sampleSize, sample, random, range, flattenDeep, times +} from 'lodash'; import { fromJS, Set as makeSet } from 'immutable'; import { hsl } from 'd3-color'; import debug from 'debug'; @@ -17,7 +19,7 @@ const STACK_VARIANTS = [false, true]; const METRIC_FILLS = [0, 0.1, 50, 99.9, 100]; const NETWORKS = [ 'be', 'fe', 'zb', 'db', 're', 'gh', 'jk', 'lol', 'nw' -].map(n => ({colorKey: n, id: n, label: n})); +].map(n => ({ colorKey: n, id: n, label: n })); const INTERNET = 'the-internet'; const LOREM = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor @@ -58,7 +60,7 @@ const deltaAdd = (name, adjacency = [], shape = 'circle', stack = false, network function addMetrics(availableMetrics, node, v) { const metrics = availableMetrics.size > 0 ? availableMetrics : fromJS([ - {id: 'host_cpu_usage_percent', label: 'CPU'} + { id: 'host_cpu_usage_percent', label: 'CPU' } ]); return Object.assign({}, node, { @@ -151,13 +153,13 @@ class DebugToolbar extends React.Component { } onChange(ev) { - this.setState({nodesToAdd: parseInt(ev.target.value, 10)}); + this.setState({ nodesToAdd: parseInt(ev.target.value, 10) }); } toggleColors() { - this.setState({ - showColors: !this.state.showColors - }); + this.setState(prevState => ({ + showColors: !prevState.showColors + })); } asyncDispatch(v) { @@ -283,46 +285,54 @@ class DebugToolbar extends React.Component {
Add nodes - - + + - - - + + - - - - + + + +
Logging - - - - - + + + + +
Colors - +
- {this.state.showColors && - - - {LABEL_PREFIXES.map(r => ( - - - ))} - -
-
} + {this.state.showColors + && ( + + + {LABEL_PREFIXES.map(r => ( + + + ))} + +
+
+ )} {this.state.showColors && [getNodeColor, getNodeColorDark].map(fn => ( @@ -330,7 +340,7 @@ class DebugToolbar extends React.Component { {LABEL_PREFIXES.map(r => ( {LABEL_PREFIXES.map(c => ( - ))} @@ -340,14 +350,20 @@ class DebugToolbar extends React.Component {
State - - + +
Short-lived nodes - - + +
); diff --git a/client/app/scripts/components/embedded-terminal.js b/client/app/scripts/components/embedded-terminal.js index 734a3c2bce..37188e196c 100644 --- a/client/app/scripts/components/embedded-terminal.js +++ b/client/app/scripts/components/embedded-terminal.js @@ -31,8 +31,8 @@ class EmeddedTerminal extends React.Component { } getTransform() { - const dx = this.state.mounted ? 0 : - window.innerWidth - DETAILS_PANEL_WIDTH - DETAILS_PANEL_MARGINS.right; + const dx = this.state.mounted ? 0 + : window.innerWidth - DETAILS_PANEL_WIDTH - DETAILS_PANEL_MARGINS.right; return `translateX(${dx}px)`; } @@ -56,7 +56,7 @@ class EmeddedTerminal extends React.Component {
+ style={{transform: this.getTransform()}}>
- {versionUpdate && - - Update available: {versionUpdate.get('version')} - + {versionUpdate + && ( + + Update available: + {' '} + {versionUpdate.get('version')} + + ) } Version {version || '...'} @@ -64,15 +68,21 @@ class Footer extends React.Component {
- -
diff --git a/client/app/scripts/components/help-panel.js b/client/app/scripts/components/help-panel.js index 445b8a4a43..a41aecc3ca 100644 --- a/client/app/scripts/components/help-panel.js +++ b/client/app/scripts/components/help-panel.js @@ -7,26 +7,26 @@ import { hideHelp } from '../actions/app-actions'; const GENERAL_SHORTCUTS = [ - {key: 'esc', label: 'Close active panel'}, - {key: '/', label: 'Activate search field'}, - {key: '?', label: 'Toggle shortcut menu'}, - {key: 'g', label: 'Switch to Graph view'}, - {key: 't', label: 'Switch to Table view'}, - {key: 'r', label: 'Switch to Resources view'}, + { key: 'esc', label: 'Close active panel' }, + { key: '/', label: 'Activate search field' }, + { key: '?', label: 'Toggle shortcut menu' }, + { key: 'g', label: 'Switch to Graph view' }, + { key: 't', label: 'Switch to Table view' }, + { key: 'r', label: 'Switch to Resources view' }, ]; const CANVAS_METRIC_SHORTCUTS = [ - {key: '<', label: 'Select and pin previous metric'}, - {key: '>', label: 'Select and pin next metric'}, - {key: 'q', label: 'Unpin current metric'}, + { key: '<', label: 'Select and pin previous metric' }, + { key: '>', label: 'Select and pin next metric' }, + { key: 'q', label: 'Unpin current metric' }, ]; function renderShortcuts(cuts) { return (
- {cuts.map(({key, label}) => ( + {cuts.map(({ key, label }) => (
{key}
{label}
@@ -51,9 +51,16 @@ function renderShortcutPanel() { const BASIC_SEARCHES = [ - {label: 'All fields for foo', term: 'foo'}, + { label: 'All fields for foo', term: 'foo' }, { - label: Any field matching pid for the value 12345, + label: ( + + Any field matching + pid + {' '} + for the value 12345 + + ), term: 'pid: 12345' }, ]; @@ -65,16 +72,37 @@ const REGEX_SEARCHES = [ term: 'foo|bar' }, { - label: command field for foobar or foobaz, + label: ( + + command + {' '} + field for foobar or foobaz + + ), term: 'command: foo(bar|baz)' }, ]; const METRIC_SEARCHES = [ - {label: CPU greater than 4%, term: 'cpu > 4%'}, { - label: Memory less than 10 megabytes, + label: ( + + CPU + {' '} + greater than 4% + + ), + term: 'cpu > 4%' + }, + { + label: ( + + Memory + {' '} + less than 10 megabytes + + ), term: 'memory < 10mb' }, ]; @@ -83,7 +111,7 @@ const METRIC_SEARCHES = [ function renderSearches(searches) { return (
- {searches.map(({term, label}) => ( + {searches.map(({ term, label }) => (
@@ -117,7 +145,7 @@ function renderSearchPanel() { function renderFieldsPanel(currentTopologyName, searchableFields) { const none = ( - None + None ); const currentTopology = ( @@ -129,8 +157,14 @@ function renderFieldsPanel(currentTopologyName, searchableFields) {

Fields and Metrics

- Searchable fields and metrics in the
- currently selected {currentTopology} topology: + Searchable fields and metrics in the + {' '} +
+ currently selected + {' '} + {currentTopology} + {' '} + topology:

@@ -162,7 +196,7 @@ function HelpPanel({ }) { return (
-
+

Help

diff --git a/client/app/scripts/components/matched-results.js b/client/app/scripts/components/matched-results.js index 7817f3f4cf..61f2659101 100644 --- a/client/app/scripts/components/matched-results.js +++ b/client/app/scripts/components/matched-results.js @@ -7,7 +7,8 @@ const Match = (searchTerms, match) => (
- {match.label}: + {match.label} +: Match(searchTerms, matches.get(fieldId))) } - {moreFieldMatches && + {moreFieldMatches + && (
{`${moreFieldMatches.size} more matches`}
+ ) }
); diff --git a/client/app/scripts/components/metric-selector.js b/client/app/scripts/components/metric-selector.js index bca5589d96..debfb0d5e2 100644 --- a/client/app/scripts/components/metric-selector.js +++ b/client/app/scripts/components/metric-selector.js @@ -22,7 +22,8 @@ class MetricSelector extends React.Component { return (
- {hasMetrics && + {hasMetrics + && (
{availableMetrics.map(metric => ( ))}
+ ) }
); diff --git a/client/app/scripts/components/node-details.js b/client/app/scripts/components/node-details.js index 50dc20d1a1..bff3c839c4 100644 --- a/client/app/scripts/components/node-details.js +++ b/client/app/scripts/components/node-details.js @@ -6,7 +6,7 @@ import { connect } from 'react-redux'; import { Map as makeMap } from 'immutable'; import { noop } from 'lodash'; -import { clickCloseDetails, clickShowTopologyForNode } from '../actions/app-actions'; +import { clickCloseDetails, clickShowTopologyForNode } from '../actions/request-actions'; import { brightenColor, getNeutralColor, getNodeColorDark } from '../utils/color-utils'; import { isGenericTable, isPropertyList } from '../utils/node-details-utils'; import { resetDocumentTitle, setDocumentTitle } from '../utils/title-utils'; @@ -56,13 +56,18 @@ class NodeDetails extends React.Component { return (
- {showSwitchTopology && + {showSwitchTopology + && ( - Show in {this.props.topologyId.replace(/-/g, ' ')} + +Show in + {this.props.topologyId.replace(/-/g, ' ')} + + ) }

- {this.props.label} not found! + {this.props.label} + {' '} +not found!

@@ -176,14 +183,17 @@ class NodeDetails extends React.Component {
- {details.parents && } + relatives={details.parents} /> + )}
- {showControls && + {showControls + && (
+ ) }
- {details.metrics && + {details.metrics + && (
Status
+ ) } - {details.metadata && + {details.metadata + && (
Info
+ ) } {details.connections && details.connections.filter(cs => cs.connections.length > 0) @@ -219,7 +234,7 @@ class NodeDetails extends React.Component { nodeIdKey="nodeId" />
- ))} + ))} {details.children && details.children.map(children => (
@@ -233,11 +248,13 @@ class NodeDetails extends React.Component {
{table.label && table.label.length > 0 && table.label} - {table.truncationCount > 0 && + {table.truncationCount > 0 + && ( + ) }
{this.renderTable(table)} @@ -266,7 +283,7 @@ class NodeDetails extends React.Component { matches={nodeMatches.get('tables')} /> ); - } else if (isPropertyList(table)) { + } if (isPropertyList(table)) { return ( { // (that is, first by row and then by column), the indexes we are interested in are of the // form columnIndex + n * columns.length, where n >= 0. Therefore we take only the values // at the index which divided by columns.length gives a reminder columnIndex. - const filteredValues = values.filter((element, index) => - index % columns.length === columnIndex); + const filteredValues = values.filter( + (element, index) => index % columns.length === columnIndex + ); // Array comparison expect(filteredValues).toEqual(expectedValues); } @@ -91,7 +92,7 @@ describe('NodeDetailsTable', () => { sortedBy="kubernetes_ip" nodeIdKey="id" nodes={nodes} - /> + /> )); @@ -127,7 +128,7 @@ describe('NodeDetailsTable', () => { sortedBy="kubernetes_namespace" nodeIdKey="id" nodes={nodes} - /> + /> )); diff --git a/client/app/scripts/components/node-details/node-details-control-button.js b/client/app/scripts/components/node-details/node-details-control-button.js index 8bca5fbb37..b79f8c3040 100644 --- a/client/app/scripts/components/node-details/node-details-control-button.js +++ b/client/app/scripts/components/node-details/node-details-control-button.js @@ -4,7 +4,7 @@ import { isEmpty } from 'lodash'; import classNames from 'classnames'; import { trackAnalyticsEvent } from '../../utils/tracking-utils'; -import { doControl } from '../../actions/app-actions'; +import { doControl } from '../../actions/request-actions'; class NodeDetailsControlButton extends React.Component { constructor(props, context) { diff --git a/client/app/scripts/components/node-details/node-details-controls.js b/client/app/scripts/components/node-details/node-details-controls.js index 41e08794f2..ca15e14989 100644 --- a/client/app/scripts/components/node-details/node-details-controls.js +++ b/client/app/scripts/components/node-details/node-details-controls.js @@ -15,11 +15,13 @@ export default function NodeDetailsControls({ return (
- {error && + {error + && (
{error}
+ ) } {sortBy(controls, 'rank').map(control => ( diff --git a/client/app/scripts/components/node-details/node-details-generic-table.js b/client/app/scripts/components/node-details/node-details-generic-table.js index 0b933ef5b6..0f9bfe5444 100644 --- a/client/app/scripts/components/node-details/node-details-generic-table.js +++ b/client/app/scripts/components/node-details/node-details-generic-table.js @@ -46,9 +46,9 @@ export default class NodeDetailsGenericTable extends React.Component { } handleLimitClick() { - this.setState({ - limit: this.state.limit ? 0 : NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT - }); + this.setState(prevState => ({ + limit: prevState.limit ? 0 : NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT + })); } render() { @@ -62,8 +62,11 @@ export default class NodeDetailsGenericTable extends React.Component { // If there are rows that would be hidden behind 'show more', keep them // expanded if any of them match the search query; otherwise hide them. if (this.state.limit > 0 && rows.length > this.state.limit) { - const hasHiddenMatch = rows.slice(this.state.limit).some(row => - columns.some(column => matches.has(genericTableEntryKey(row, column)))); + const hasHiddenMatch = rows + .slice(this.state.limit) + .some( + row => columns.some(column => matches.has(genericTableEntryKey(row, column))) + ); if (!hasHiddenMatch) { notShown = rows.length - NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT; rows = rows.slice(0, this.state.limit); @@ -94,15 +97,17 @@ export default class NodeDetailsGenericTable extends React.Component { title={value} key={column.id} style={styles[index]}> - {column.dataType === 'link' ? - - {value} - : - + {column.dataType === 'link' + ? ( + + {value} + + ) + : } ); diff --git a/client/app/scripts/components/node-details/node-details-health.js b/client/app/scripts/components/node-details/node-details-health.js index e73b2b0d5b..4c724b8784 100644 --- a/client/app/scripts/components/node-details/node-details-health.js +++ b/client/app/scripts/components/node-details/node-details-health.js @@ -13,8 +13,9 @@ export default class NodeDetailsHealth extends React.Component { } handleClickMore() { - const expanded = !this.state.expanded; - this.setState({expanded}); + this.setState(prevState => ({ + expanded: !prevState.expanded + })); } render() { @@ -38,18 +39,22 @@ export default class NodeDetailsHealth extends React.Component { return (
- {shownWithData.map(item => ())} + {shownWithData.map(item => ( + + ))}
- {shownEmpty.map(item => ())} + {shownEmpty.map(item => ( + + ))}
({ + expanded: !prevState.expanded + })); } render() { @@ -47,18 +48,22 @@ class NodeDetailsInfo extends React.Component { {field.label}
- {field.dataType === 'link' ? - - {value} - : - + {field.dataType === 'link' + ? ( + + {value} + + ) + : ( + + ) }
diff --git a/client/app/scripts/components/node-details/node-details-property-list.js b/client/app/scripts/components/node-details/node-details-property-list.js index 92a7fbc22e..95585fe16a 100644 --- a/client/app/scripts/components/node-details/node-details-property-list.js +++ b/client/app/scripts/components/node-details/node-details-property-list.js @@ -9,10 +9,12 @@ import ShowMore from '../show-more'; const Controls = controls => (
- {sortBy(controls, 'rank').map(control => ())} + {sortBy(controls, 'rank').map(control => ( + + ))}
); @@ -26,8 +28,9 @@ export default class NodeDetailsPropertyList extends React.Component { } handleLimitClick() { - const limit = this.state.limit ? 0 : NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT; - this.setState({limit}); + this.setState(prevState => ({ + limit: prevState.limit ? 0 : NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT + })); } render() { diff --git a/client/app/scripts/components/node-details/node-details-relatives-link.js b/client/app/scripts/components/node-details/node-details-relatives-link.js index 2750fb86ed..77650b0c0a 100644 --- a/client/app/scripts/components/node-details/node-details-relatives-link.js +++ b/client/app/scripts/components/node-details/node-details-relatives-link.js @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; -import { clickRelative } from '../../actions/app-actions'; +import { clickRelative } from '../../actions/request-actions'; import { trackAnalyticsEvent } from '../../utils/tracking-utils'; import MatchedText from '../matched-text'; diff --git a/client/app/scripts/components/node-details/node-details-relatives.js b/client/app/scripts/components/node-details/node-details-relatives.js index ec9d6dedf1..e4af56a0dd 100644 --- a/client/app/scripts/components/node-details/node-details-relatives.js +++ b/client/app/scripts/components/node-details/node-details-relatives.js @@ -15,8 +15,9 @@ export default class NodeDetailsRelatives extends React.Component { handleLimitClick(ev) { ev.preventDefault(); - const limit = this.state.limit ? 0 : NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT; - this.setState({limit}); + this.setState(prevState => ({ + limit: prevState.limit ? 0 : NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT + })); } render() { @@ -37,13 +38,16 @@ export default class NodeDetailsRelatives extends React.Component { ))} - {showLimitAction && - - {limitActionText} - + {...relative} /> + ))} + {showLimitAction + && ( + + {limitActionText} + + ) }
); diff --git a/client/app/scripts/components/node-details/node-details-table-headers.js b/client/app/scripts/components/node-details/node-details-table-headers.js index 37a4a77792..48a049de5a 100644 --- a/client/app/scripts/components/node-details/node-details-table-headers.js +++ b/client/app/scripts/components/node-details/node-details-table-headers.js @@ -33,9 +33,9 @@ export default class NodeDetailsTableHeaders extends React.Component { } const style = colStyles[index]; - const label = - (style.width === NODE_DETAILS_TABLE_CW.XS && NODE_DETAILS_TABLE_XS_LABEL[header.id]) ? - NODE_DETAILS_TABLE_XS_LABEL[header.id] : header.label; + const label = ( + style.width === NODE_DETAILS_TABLE_CW.XS && NODE_DETAILS_TABLE_XS_LABEL[header.id] + ) ? NODE_DETAILS_TABLE_XS_LABEL[header.id] : header.label; return (
); } @@ -72,13 +75,14 @@ function renderValues(node, columns = [], columnStyles = [], timestamp = null, t title={field.value} style={style} key={field.id}> - {intersperse(field.relatives.map(relative => - ( ( + )), ' ')} + /> + )), ' ')} ); } @@ -98,19 +102,6 @@ function renderValues(node, columns = [], columnStyles = [], timestamp = null, t }); } -/** - * Table row children may react to onClick events but the row - * itself does detect a click by looking at onMouseUp. To stop - * the bubbling of clicks on child elements we need to dismiss - * the onMouseUp event. - */ -export const dismissRowClickProps = { - onMouseUp: (ev) => { - ev.preventDefault(); - ev.stopPropagation(); - } -}; - export default class NodeDetailsTableRow extends React.Component { constructor(props, context) { super(props, context); @@ -150,8 +141,8 @@ export default class NodeDetailsTableRow extends React.Component { const { pageX, pageY } = ev; const { originX, originY } = this.mouseDrag; const movedTheMouseTooMuch = ( - Math.abs(originX - pageX) > thresholdPx || - Math.abs(originY - pageY) > thresholdPx + Math.abs(originX - pageX) > thresholdPx + || Math.abs(originY - pageY) > thresholdPx ); if (movedTheMouseTooMuch && originX && originY) { return; @@ -182,7 +173,7 @@ export default class NodeDetailsTableRow extends React.Component { onMouseLeave={this.onMouseLeave} className={className}> {values} diff --git a/client/app/scripts/components/node-details/node-details-table.js b/client/app/scripts/components/node-details/node-details-table.js index d235541d4e..4af2e1b8dd 100644 --- a/client/app/scripts/components/node-details/node-details-table.js +++ b/client/app/scripts/components/node-details/node-details-table.js @@ -1,7 +1,9 @@ import React from 'react'; import classNames from 'classnames'; import { connect } from 'react-redux'; -import { find, get, union, sortBy, groupBy, concat, debounce } from 'lodash'; +import { + find, get, union, sortBy, groupBy, concat, debounce +} from 'lodash'; import { NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT } from '../../constants/limits'; import { TABLE_ROW_FOCUS_DEBOUNCE_INTERVAL } from '../../constants/timer'; @@ -18,7 +20,7 @@ import { function getDefaultSortedBy(columns, nodes) { // default sorter specified by columns - const defaultSortColumn = find(columns, {defaultSort: true}); + const defaultSortColumn = find(columns, { defaultSort: true }); if (defaultSortColumn) { return defaultSortColumn.id; } @@ -49,7 +51,7 @@ function getNodeValue(node, header) { if (isIP(header)) { // Format the IPs so that they are sorted numerically. return ipToPaddedString(field.value); - } else if (isNumeric(header)) { + } if (isNumeric(header)) { return parseFloat(field.value); } return field.value; @@ -125,7 +127,7 @@ function getSortedNodes(nodes, sortedByHeader, sortedDesc) { // have a minimal height. That prevents auto-scroll under a focus if the // number of table rows shrinks. function minHeightConstraint(height = 0) { - return ; + return ; } @@ -158,8 +160,9 @@ class NodeDetailsTable extends React.Component { } handleLimitClick() { - const limit = this.state.limit ? 0 : this.props.limit; - this.setState({ limit }); + this.setState(prevState => ({ + limit: prevState.limit ? 0 : this.props.limit + })); } focusRow(rowIndex, node) { @@ -202,7 +205,7 @@ class NodeDetailsTable extends React.Component { getColumnHeaders() { const columns = this.props.columns || []; - return [{id: 'label', label: this.props.label}].concat(columns); + return [{ id: 'label', label: this.props.label }].concat(columns); } componentDidMount() { @@ -218,8 +221,8 @@ class NodeDetailsTable extends React.Component { const sortedBy = this.state.sortedBy || getDefaultSortedBy(columns, this.props.nodes); const sortedByHeader = this.getColumnHeaders().find(h => h.id === sortedBy); - const sortedDesc = (this.state.sortedDesc === null) ? - defaultSortDesc(sortedByHeader) : this.state.sortedDesc; + const sortedDesc = (this.state.sortedDesc === null) + ? defaultSortDesc(sortedByHeader) : this.state.sortedDesc; let nodes = getSortedNodes(this.props.nodes, sortedByHeader, sortedDesc); @@ -261,12 +264,14 @@ class NodeDetailsTable extends React.Component {
+ ))}
diff --git a/client/app/scripts/components/node-details/node-details-table-node-link.js b/client/app/scripts/components/node-details/node-details-table-node-link.js index 1c3303b9b5..3d4f3f614b 100644 --- a/client/app/scripts/components/node-details/node-details-table-node-link.js +++ b/client/app/scripts/components/node-details/node-details-table-node-link.js @@ -1,9 +1,9 @@ import React from 'react'; import { connect } from 'react-redux'; -import { clickRelative } from '../../actions/app-actions'; +import { clickRelative } from '../../actions/request-actions'; import { trackAnalyticsEvent } from '../../utils/tracking-utils'; -import { dismissRowClickProps } from './node-details-table-row'; +import { dismissRowClickProps } from '../../utils/dom-utils'; class NodeDetailsTableNodeLink extends React.Component { diff --git a/client/app/scripts/components/node-details/node-details-table-node-metric-link.js b/client/app/scripts/components/node-details/node-details-table-node-metric-link.js index 3749fce826..e097adf479 100644 --- a/client/app/scripts/components/node-details/node-details-table-node-metric-link.js +++ b/client/app/scripts/components/node-details/node-details-table-node-metric-link.js @@ -3,7 +3,7 @@ import React from 'react'; import CloudLink from '../cloud-link'; import { formatMetric } from '../../utils/string-utils'; import { trackAnalyticsEvent } from '../../utils/tracking-utils'; -import { dismissRowClickProps } from './node-details-table-row'; +import { dismissRowClickProps } from '../../utils/dom-utils'; class NodeDetailsTableNodeMetricLink extends React.Component { constructor(props) { diff --git a/client/app/scripts/components/node-details/node-details-table-row.js b/client/app/scripts/components/node-details/node-details-table-row.js index 15553d4a9a..8e452a4d5c 100644 --- a/client/app/scripts/components/node-details/node-details-table-row.js +++ b/client/app/scripts/components/node-details/node-details-table-row.js @@ -54,14 +54,17 @@ function renderValues(node, columns = [], columnStyles = [], timestamp = null, t title={title} style={style} key={field.id}> - {field.dataType === 'link' ? - {value} - : - value} + {field.dataType === 'link' + ? ( + + {value} + + ) + : value} - {this.props.renderIdCell(Object.assign(node, {nodeId, topologyId}))} + {this.props.renderIdCell(Object.assign(node, { nodeId, topologyId }))}
- {this.props.nodes && this.props.nodes.length > 0 && } + {this.props.nodes && this.props.nodes.length > 0 && ( + + )} {}, + onSortChange: () => { }, sortedBy: null, sortedDesc: null, }; diff --git a/client/app/scripts/components/nodes-resources/node-resources-layer.js b/client/app/scripts/components/nodes-resources/node-resources-layer.js index 96a47f7aa5..ef21627af4 100644 --- a/client/app/scripts/components/nodes-resources/node-resources-layer.js +++ b/client/app/scripts/components/nodes-resources/node-resources-layer.js @@ -35,11 +35,13 @@ class NodesResourcesLayer extends React.Component { /> ))} - {!layoutNodes.isEmpty() && } + /> + )} ); } diff --git a/client/app/scripts/components/nodes-resources/node-resources-metric-box-info.js b/client/app/scripts/components/nodes-resources/node-resources-metric-box-info.js index e165401678..6596759271 100644 --- a/client/app/scripts/components/nodes-resources/node-resources-metric-box-info.js +++ b/client/app/scripts/components/nodes-resources/node-resources-metric-box-info.js @@ -13,11 +13,21 @@ export default class NodeResourcesMetricBoxInfo extends React.Component { {showExtendedInfo ? humanizedRelativeConsumption : humanizedAbsoluteConsumption} - used - {showExtendedInfo && + + {' '} +used + {showExtendedInfo + && ( - {' - '}({humanizedAbsoluteConsumption} / {humanizedTotalCapacity}) + {' - '} +( + {humanizedAbsoluteConsumption} + {' '} +/ + {humanizedTotalCapacity} +) + ) } ); diff --git a/client/app/scripts/components/nodes-resources/node-resources-metric-box.js b/client/app/scripts/components/nodes-resources/node-resources-metric-box.js index 161257e1bf..073afaee26 100644 --- a/client/app/scripts/components/nodes-resources/node-resources-metric-box.js +++ b/client/app/scripts/components/nodes-resources/node-resources-metric-box.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import theme from 'weaveworks-ui-components/lib/theme'; import NodeResourcesMetricBoxInfo from './node-resources-metric-box-info'; -import { clickNode } from '../../actions/app-actions'; +import { clickNode } from '../../actions/request-actions'; import { trackAnalyticsEvent } from '../../utils/tracking-utils'; import { applyTransform } from '../../utils/transform-utils'; import { RESOURCE_VIEW_MODE } from '../../constants/naming'; @@ -112,9 +112,9 @@ class NodeResourcesMetricBox extends React.Component { // TODO: Show `+ 31 nodes` kind of tag in their stead. if (!showNode) return null; - const resourceUsageTooltipInfo = showCapacity ? - metricSummary.get('humanizedRelativeConsumption') : - metricSummary.get('humanizedAbsoluteConsumption'); + const resourceUsageTooltipInfo = showCapacity + ? metricSummary.get('humanizedRelativeConsumption') + : metricSummary.get('humanizedAbsoluteConsumption'); return ( - {label} - {type} usage at {resourceUsageTooltipInfo} - {showCapacity && + {label} + {' '} +- + {' '} + {type} + {' '} +usage at + {' '} + {resourceUsageTooltipInfo} + + {showCapacity && ( + } + /> + )} - {showInfo && } + /> + )} ); } diff --git a/client/app/scripts/components/nodes.js b/client/app/scripts/components/nodes.js index e7ec56c52a..cfd61ec7ca 100644 --- a/client/app/scripts/components/nodes.js +++ b/client/app/scripts/components/nodes.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import NodesChart from '../charts/nodes-chart'; import NodesGrid from '../charts/nodes-grid'; -import NodesResources from '../components/nodes-resources'; +import NodesResources from './nodes-resources'; import NodesError from '../charts/nodes-error'; import DelayedShow from '../utils/delayed-show'; import { Loading, getNodeType } from './loading'; @@ -44,9 +44,9 @@ class Nodes extends React.Component { return ( ); } diff --git a/client/app/scripts/components/plugins.js b/client/app/scripts/components/plugins.js index 7537553497..3a56f9f50e 100644 --- a/client/app/scripts/components/plugins.js +++ b/client/app/scripts/components/plugins.js @@ -10,7 +10,16 @@ const Plugin = ({ }) => { const error = status !== 'ok'; const className = classNames({ error }); - const tip = (Description: {description}
Status: {status}
); + const tip = ( + +Description: + {description} +
+Status: + {' '} + {status} +
+ ); // Inner span to hold styling so we don't effect the "before:content" return ( diff --git a/client/app/scripts/components/search.js b/client/app/scripts/components/search.js index 193da88019..bd0b091521 100644 --- a/client/app/scripts/components/search.js +++ b/client/app/scripts/components/search.js @@ -4,7 +4,12 @@ import { isEmpty } from 'lodash'; import { Search } from 'weaveworks-ui-components'; import styled from 'styled-components'; -import { blurSearch, focusSearch, updateSearch, toggleHelp } from '../actions/app-actions'; +import { + blurSearch, updateSearch, toggleHelp +} from '../actions/app-actions'; +import { + focusSearch +} from '../actions/request-actions'; import { searchMatchCountByTopologySelector } from '../selectors/search'; import { isResourceViewModeSelector } from '../selectors/topology'; import { slugify } from '../utils/string-utils'; @@ -107,7 +112,9 @@ class SearchComponent extends React.Component { onBlur={this.props.blurSearch} /> - {searchHint} diff --git a/client/app/scripts/components/show-more.js b/client/app/scripts/components/show-more.js index 15868ef625..3744dabe60 100644 --- a/client/app/scripts/components/show-more.js +++ b/client/app/scripts/components/show-more.js @@ -24,7 +24,9 @@ export default class ShowMore extends React.PureComponent { } return (
- {limitActionText} + {limitActionText} + {' '} +
); } diff --git a/client/app/scripts/components/sparkline.js b/client/app/scripts/components/sparkline.js index 9ac381a61c..e5b814fbdc 100644 --- a/client/app/scripts/components/sparkline.js +++ b/client/app/scripts/components/sparkline.js @@ -72,8 +72,8 @@ export default class Sparkline extends React.Component { const min = formatMetricSvg(d3Min(data, d => d.value), this.props); const max = formatMetricSvg(d3Max(data, d => d.value), this.props); const mean = formatMetricSvg(d3Mean(data, d => d.value), this.props); - const title = `Last ${Math.round((lastDate - firstDate) / 1000)} seconds, ` + - `${data.length} samples, min: ${min}, max: ${max}, mean: ${mean}`; + const title = `Last ${Math.round((lastDate - firstDate) / 1000)} seconds, ` + + `${data.length} samples, min: ${min}, max: ${max}, mean: ${mean}`; return { data, lastX, lastY, title @@ -122,7 +122,8 @@ export default class Sparkline extends React.Component { strokeDasharray={strokeDasharray} d={this.line(graph.data)} /> - {hasData && } + /> + )} ); diff --git a/client/app/scripts/components/terminal-app.js b/client/app/scripts/components/terminal-app.js index 98484f6ef6..d8819aa5ff 100644 --- a/client/app/scripts/components/terminal-app.js +++ b/client/app/scripts/components/terminal-app.js @@ -51,12 +51,14 @@ class TerminalApp extends React.Component { return (
- {this.props.controlPipe && } + embedded={false} /> + )}
); } diff --git a/client/app/scripts/components/terminal.js b/client/app/scripts/components/terminal.js index 5e8d874d90..5dda94fd7d 100644 --- a/client/app/scripts/components/terminal.js +++ b/client/app/scripts/components/terminal.js @@ -8,9 +8,12 @@ import { Terminal as Term } from 'xterm'; import * as fit from 'xterm/lib/addons/fit/fit'; import { closeTerminal } from '../actions/app-actions'; +import { getPipeStatus } from '../actions/request-actions'; import { getNeutralColor } from '../utils/color-utils'; import { setDocumentTitle } from '../utils/title-utils'; -import { getPipeStatus, deletePipe, doResizeTty, getWebsocketUrl, basePath } from '../utils/web-api-utils'; +import { + deletePipe, doResizeTty, getWebsocketUrl, basePath +} from '../utils/web-api-utils'; const log = debug('scope:terminal'); @@ -327,6 +330,7 @@ class Terminal extends React.Component { ); } + getControlStatusIcon() { const icon = this.props.controlStatus && this.props.controlStatus.get('control').icon; return ( @@ -339,8 +343,7 @@ class Terminal extends React.Component { } function mapStateToProps(state, ownProps) { - const controlStatus = state.get('controlPipes').find(pipe => - pipe.get('nodeId') === ownProps.pipe.get('nodeId')); + const controlStatus = state.get('controlPipes').find(pipe => pipe.get('nodeId') === ownProps.pipe.get('nodeId')); return { controlStatus }; } diff --git a/client/app/scripts/components/time-control.js b/client/app/scripts/components/time-control.js index 59593447ea..b868599bf6 100644 --- a/client/app/scripts/components/time-control.js +++ b/client/app/scripts/components/time-control.js @@ -4,7 +4,7 @@ import { connect } from 'react-redux'; import { TimestampTag } from 'weaveworks-ui-components'; import { trackAnalyticsEvent } from '../utils/tracking-utils'; -import { pauseTimeAtNow, resumeTime } from '../actions/app-actions'; +import { pauseTimeAtNow, resumeTime } from '../actions/request-actions'; import { isPausedSelector, timeTravelSupportedSelector } from '../selectors/time-travel'; @@ -64,10 +64,14 @@ class TimeControl extends React.Component { - {isPaused && + {isPaused + && ( - Showing state from + Showing state from + {' '} + + ) } ); diff --git a/client/app/scripts/components/time-travel-wrapper.js b/client/app/scripts/components/time-travel-wrapper.js index ee749ece49..ba3c1fe4a4 100644 --- a/client/app/scripts/components/time-travel-wrapper.js +++ b/client/app/scripts/components/time-travel-wrapper.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { TimeTravel } from 'weaveworks-ui-components'; -import { jumpToTime, resumeTime, pauseTimeAtNow } from '../actions/app-actions'; +import { jumpToTime, resumeTime, pauseTimeAtNow } from '../actions/request-actions'; class TimeTravelWrapper extends React.Component { handleLiveModeChange = (showingLive) => { diff --git a/client/app/scripts/components/topologies.js b/client/app/scripts/components/topologies.js index aa74286c39..617efe2574 100644 --- a/client/app/scripts/components/topologies.js +++ b/client/app/scripts/components/topologies.js @@ -5,7 +5,7 @@ import classnames from 'classnames'; import { trackAnalyticsEvent } from '../utils/tracking-utils'; import { searchMatchCountByTopologySelector } from '../selectors/search'; import { isResourceViewModeSelector } from '../selectors/topology'; -import { clickTopology } from '../actions/app-actions'; +import { clickTopology } from '../actions/request-actions'; function basicTopologyInfo(topology, searchMatchCount) { diff --git a/client/app/scripts/components/topology-options.js b/client/app/scripts/components/topology-options.js index 29896023b3..bbc0c5f435 100644 --- a/client/app/scripts/components/topology-options.js +++ b/client/app/scripts/components/topology-options.js @@ -7,7 +7,7 @@ import { trackAnalyticsEvent } from '../utils/tracking-utils'; import { getCurrentTopologyOptions } from '../utils/topology-utils'; import { activeTopologyOptionsSelector } from '../selectors/topology'; import TopologyOptionAction from './topology-option-action'; -import { changeTopologyOption } from '../actions/app-actions'; +import { changeTopologyOption } from '../actions/request-actions'; class TopologyOptions extends React.Component { constructor(props, context) { @@ -101,7 +101,8 @@ class TopologyOptions extends React.Component { return (
- {option.get('selectType') === 'union' && + {option.get('selectType') === 'union' + && ( + ) } {option.get('options').map(item => ( Save raw data as JSON - {pausedAt && ({pausedAt})} + {pausedAt && ( + + {' '} + ( + {pausedAt} + ) + + )}
-
diff --git a/client/app/scripts/components/zoomable-canvas.js b/client/app/scripts/components/zoomable-canvas.js index f9801ae2b4..51b51c73e8 100644 --- a/client/app/scripts/components/zoomable-canvas.js +++ b/client/app/scripts/components/zoomable-canvas.js @@ -8,8 +8,8 @@ import { drag } from 'd3-drag'; import { event as d3Event, select } from 'd3-selection'; import { zoomFactor } from 'weaveworks-ui-components/lib/utils/zooming'; -import Logo from '../components/logo'; -import ZoomControl from '../components/zoom-control'; +import Logo from './logo'; +import ZoomControl from './zoom-control'; import { cacheZoomState } from '../actions/app-actions'; import { applyTransform, inverseTransform } from '../utils/transform-utils'; import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/zooming'; @@ -116,12 +116,14 @@ class ZoomableCanvas extends React.Component { {this.props.children(this.state)} - {this.canChangeZoom() && } + /> + )}
); } diff --git a/client/app/scripts/decorators/node.js b/client/app/scripts/decorators/node.js index 2126605c05..ecafa27be5 100644 --- a/client/app/scripts/decorators/node.js +++ b/client/app/scripts/decorators/node.js @@ -14,9 +14,9 @@ export function nodeResourceViewColorDecorator(node) { // Decorates the resource node with dimensions taken from its metric summary. export function nodeResourceBoxDecorator(node) { const metricSummary = node.get('metricSummary', makeMap()); - const width = metricSummary.get('showCapacity') ? - metricSummary.get('totalCapacity') : - metricSummary.get('absoluteConsumption'); + const width = metricSummary.get('showCapacity') + ? metricSummary.get('totalCapacity') + : metricSummary.get('absoluteConsumption'); const height = RESOURCES_LAYER_HEIGHT; return node.merge(makeMap({ height, width })); diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index 9ebebae6f4..2cfd47d47b 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -1,7 +1,9 @@ /* eslint-disable import/no-webpack-loader-syntax, import/no-unresolved */ import debug from 'debug'; import moment from 'moment'; -import { size, each, includes, isEqual } from 'lodash'; +import { + size, each, includes, isEqual +} from 'lodash'; import { fromJS, is as isDeepEqual, diff --git a/client/app/scripts/router.js b/client/app/scripts/router.js new file mode 100644 index 0000000000..2b20190cc7 --- /dev/null +++ b/client/app/scripts/router.js @@ -0,0 +1,64 @@ +import page from 'page'; +import stableStringify from 'json-stable-stringify'; +import { each } from 'lodash'; + +import { route } from './actions/request-actions'; +import { storageGet, storageSet } from './utils/storage-utils'; +import { + decodeURL, encodeURL, isStoreViewStateEnabled, STORAGE_STATE_KEY +} from './utils/router-utils'; + +// Temporarily detect old topology options to avoid breaking things between releases +// Related to https://github.com/weaveworks/scope/pull/2404 +function detectOldOptions(topologyOptions) { + let bad = false; + each(topologyOptions, (topology) => { + each(topology, (option) => { + if (typeof option === 'string') { + bad = true; + } + }); + }); + return bad; +} + +export function getRouter(initialState) { + return (dispatch, getState) => { + // strip any trailing '/'s. + page.base(window.location.pathname.replace(/\/$/, '')); + + page('/', () => { + // recover from storage state on empty URL + const storageState = storageGet(STORAGE_STATE_KEY); + if (storageState && isStoreViewStateEnabled(getState())) { + const parsedState = JSON.parse(decodeURL(storageState)); + const dirtyOptions = detectOldOptions(parsedState.topologyOptions); + if (dirtyOptions) { + dispatch(route(initialState)); + } else { + const mergedState = Object.assign(initialState, parsedState); + // push storage state to URL + window.location.hash = `!/state/${stableStringify(mergedState)}`; + dispatch(route(mergedState)); + } + } else { + dispatch(route(initialState)); + } + }); + + page('/state/:state', (ctx) => { + const state = JSON.parse(decodeURL(ctx.params.state)); + const dirtyOptions = detectOldOptions(state.topologyOptions); + const nextState = dirtyOptions ? initialState : state; + + // back up state in storage and redirect + if (isStoreViewStateEnabled(getState())) { + storageSet(STORAGE_STATE_KEY, encodeURL(stableStringify(state))); + } + + dispatch(route(nextState)); + }); + + return page; + }; +} diff --git a/client/app/scripts/selectors/node-metric.js b/client/app/scripts/selectors/node-metric.js index 4058534fd1..d788d99d4f 100644 --- a/client/app/scripts/selectors/node-metric.js +++ b/client/app/scripts/selectors/node-metric.js @@ -3,7 +3,7 @@ import { createMapSelector, createListSelector } from 'reselect-map'; import { fromJS, Map as makeMap, List as makeList } from 'immutable'; import { modulo } from '../utils/math-utils'; -import { isGraphViewModeSelector, isResourceViewModeSelector } from '../selectors/topology'; +import { isGraphViewModeSelector, isResourceViewModeSelector } from './topology'; import { RESOURCE_VIEW_METRICS } from '../constants/resources'; @@ -93,8 +93,7 @@ const selectedMetricIdSelector = createSelector( availableMetricsSelector, selectedMetricTypeSelector, ], - (availableMetrics, metricType) => - (availableMetrics.find(m => m.get('label') === metricType) || makeMap()).get('id') + (availableMetrics, metricType) => (availableMetrics.find(m => m.get('label') === metricType) || makeMap()).get('id') ); const topCardNodeSelector = createSelector( diff --git a/client/app/scripts/selectors/resource-view/layout.js b/client/app/scripts/selectors/resource-view/layout.js index 44d41783b0..c2f6b42cd0 100644 --- a/client/app/scripts/selectors/resource-view/layout.js +++ b/client/app/scripts/selectors/resource-view/layout.js @@ -22,9 +22,9 @@ const log = debug('scope:nodes-layout'); // Used for ordering the resource nodes. const resourceNodeConsumptionComparator = (node) => { const metricSummary = node.get('metricSummary'); - return metricSummary.get('showCapacity') ? - -metricSummary.get('relativeConsumption') : - -metricSummary.get('absoluteConsumption'); + return metricSummary.get('showCapacity') + ? -metricSummary.get('relativeConsumption') + : -metricSummary.get('absoluteConsumption'); }; // A list of topologies shown in the resource view of the active topology (bottom to top). @@ -85,8 +85,10 @@ const decoratedNodesByTopologySelector = createSelector( const isBaseLayer = (index === 0); const nodeParentDecorator = nodeParentDecoratorByTopologyId(parentLayerTopologyId); - const nodeMetricSummaryDecorator = - nodeMetricSummaryDecoratorByType(pinnedMetricType, showCapacity); + const nodeMetricSummaryDecorator = nodeMetricSummaryDecoratorByType( + pinnedMetricType, + showCapacity + ); // Color the node, deduce its anchor point, dimensions and info about its pinned metric. const decoratedTopologyNodes = (topologyNodes || makeMap()) @@ -158,8 +160,8 @@ export const layoutNodesByTopologyIdSelector = createSelector( // We fix it by shrinking all the children to by a factor to perfectly fit into the parent. if (totalChildrenWidth > parentWidth) { const shrinkFactor = parentWidth / totalChildrenWidth; - log(`Inconsistent data: Children of ${parentNodeId} reported to use more ` + - `resource than the node itself - shrinking by factor ${shrinkFactor}`); + log(`Inconsistent data: Children of ${parentNodeId} reported to use more ` + + `resource than the node itself - shrinking by factor ${shrinkFactor}`); // Shrink all the children. nodesBucket.forEach((_, nodeId) => { let node = positionedNodes.get(nodeId); diff --git a/client/app/scripts/selectors/search.js b/client/app/scripts/selectors/search.js index 00b16b1bbf..7e0194c663 100644 --- a/client/app/scripts/selectors/search.js +++ b/client/app/scripts/selectors/search.js @@ -2,7 +2,9 @@ import { createSelector } from 'reselect'; import { createMapSelector } from 'reselect-map'; import { Map as makeMap } from 'immutable'; -import { parseQuery, searchNode, searchTopology, getSearchableFields } from '../utils/search-utils'; +import { + parseQuery, searchNode, searchTopology, getSearchableFields +} from '../utils/search-utils'; const parsedSearchQuerySelector = createSelector( diff --git a/client/app/scripts/utils/__tests__/web-api-utils-test.js b/client/app/scripts/utils/__tests__/web-api-utils-test.js index da32769c4d..1b78c1fc88 100644 --- a/client/app/scripts/utils/__tests__/web-api-utils-test.js +++ b/client/app/scripts/utils/__tests__/web-api-utils-test.js @@ -1,6 +1,8 @@ import { Map as makeMap, OrderedMap as makeOrderedMap } from 'immutable'; -import { buildUrlQuery, basePath, getApiPath, getWebsocketUrl } from '../web-api-utils'; +import { + buildUrlQuery, basePath, getApiPath, getWebsocketUrl +} from '../web-api-utils'; describe('WebApiUtils', () => { diff --git a/client/app/scripts/utils/array-utils.js b/client/app/scripts/utils/array-utils.js index 4088b82a33..dfcfeef7bf 100644 --- a/client/app/scripts/utils/array-utils.js +++ b/client/app/scripts/utils/array-utils.js @@ -7,8 +7,8 @@ export function uniformSelect(array, size) { return array; } - return range(size).map(index => - array[parseInt(index * (array.length / (size - (1 - 1e-9))), 10)]); + return range(size) + .map(index => array[parseInt(index * (array.length / (size - (1 - 1e-9))), 10)]); } export function insertElement(array, index, element) { diff --git a/client/app/scripts/utils/dom-utils.js b/client/app/scripts/utils/dom-utils.js index b993a51361..1ab2bcf367 100644 --- a/client/app/scripts/utils/dom-utils.js +++ b/client/app/scripts/utils/dom-utils.js @@ -14,3 +14,16 @@ export function encodeIdAttribute(id) { export function decodeIdAttribute(id) { return id.replace(/__u(\d+)__/gm, (m, d) => String.fromCharCode(d)); } + +/** + * Table row children may react to onClick events but the row + * itself does detect a click by looking at onMouseUp. To stop + * the bubbling of clicks on child elements we need to dismiss + * the onMouseUp event. + */ +export const dismissRowClickProps = { + onMouseUp: (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + } +}; diff --git a/client/app/scripts/utils/hash-utils.js b/client/app/scripts/utils/hash-utils.js index 31cbbb2166..76c358d386 100644 --- a/client/app/scripts/utils/hash-utils.js +++ b/client/app/scripts/utils/hash-utils.js @@ -1,4 +1,6 @@ -import { isPlainObject, mapValues, isEmpty, omitBy } from 'lodash'; +import { + isPlainObject, mapValues, isEmpty, omitBy +} from 'lodash'; export function hashDifferenceDeep(A, B) { diff --git a/client/app/scripts/utils/metric-utils.js b/client/app/scripts/utils/metric-utils.js index 8bdad12c2c..d7731f46c5 100644 --- a/client/app/scripts/utils/metric-utils.js +++ b/client/app/scripts/utils/metric-utils.js @@ -57,12 +57,12 @@ export function getMetricColor(metric) { : metric && metric.get('id'); if (/mem/.test(metricId)) { return 'steelBlue'; - } else if (/cpu/.test(metricId)) { + } if (/cpu/.test(metricId)) { return colors('cpu').toString(); - } else if (/files/.test(metricId)) { + } if (/files/.test(metricId)) { // purple return '#9467bd'; - } else if (/load/.test(metricId)) { + } if (/load/.test(metricId)) { return colors('load').toString(); } return 'steelBlue'; diff --git a/client/app/scripts/utils/router-utils.js b/client/app/scripts/utils/router-utils.js index 58f376ffae..aa339ed521 100644 --- a/client/app/scripts/utils/router-utils.js +++ b/client/app/scripts/utils/router-utils.js @@ -1,11 +1,10 @@ import page from 'page'; import stableStringify from 'json-stable-stringify'; import { fromJS, is as isDeepEqual } from 'immutable'; -import { each, omit, omitBy, isEmpty } from 'lodash'; +import { omit, omitBy, isEmpty } from 'lodash'; -import { route } from '../actions/app-actions'; import { hashDifferenceDeep } from './hash-utils'; -import { storageGet, storageSet } from './storage-utils'; +import { storageSet } from './storage-utils'; import { getDefaultTopologyOptions, initialState as initialRootState } from '../reducers/root'; @@ -17,9 +16,9 @@ const SLASH = '/'; const SLASH_REPLACEMENT = ''; const PERCENT = '%'; const PERCENT_REPLACEMENT = ''; -const STORAGE_STATE_KEY = 'scopeViewState'; +export const STORAGE_STATE_KEY = 'scopeViewState'; -function encodeURL(url) { +export function encodeURL(url) { return url .replace(new RegExp(PERCENT, 'g'), PERCENT_REPLACEMENT) .replace(new RegExp(SLASH, 'g'), SLASH_REPLACEMENT); @@ -41,7 +40,7 @@ export function clearStoredViewState() { storageSet(STORAGE_STATE_KEY, ''); } -function isStoreViewStateEnabled(state) { +export function isStoreViewStateEnabled(state) { return state.get('storeViewState'); } @@ -135,59 +134,3 @@ export function updateRoute(getState) { page.show(`/state/${stateUrl}`, state, dispatch); } } - -// Temporarily detect old topology options to avoid breaking things between releases -// Related to https://github.com/weaveworks/scope/pull/2404 -function detectOldOptions(topologyOptions) { - let bad = false; - each(topologyOptions, (topology) => { - each(topology, (option) => { - if (typeof option === 'string') { - bad = true; - } - }); - }); - return bad; -} - - -export function getRouter(initialState) { - return (dispatch, getState) => { - // strip any trailing '/'s. - page.base(window.location.pathname.replace(/\/$/, '')); - - page('/', () => { - // recover from storage state on empty URL - const storageState = storageGet(STORAGE_STATE_KEY); - if (storageState && isStoreViewStateEnabled(getState())) { - const parsedState = JSON.parse(decodeURL(storageState)); - const dirtyOptions = detectOldOptions(parsedState.topologyOptions); - if (dirtyOptions) { - dispatch(route(initialState)); - } else { - const mergedState = Object.assign(initialState, parsedState); - // push storage state to URL - window.location.hash = `!/state/${stableStringify(mergedState)}`; - dispatch(route(mergedState)); - } - } else { - dispatch(route(initialState)); - } - }); - - page('/state/:state', (ctx) => { - const state = JSON.parse(decodeURL(ctx.params.state)); - const dirtyOptions = detectOldOptions(state.topologyOptions); - const nextState = dirtyOptions ? initialState : state; - - // back up state in storage and redirect - if (isStoreViewStateEnabled(getState())) { - storageSet(STORAGE_STATE_KEY, encodeURL(stableStringify(state))); - } - - dispatch(route(nextState)); - }); - - return page; - }; -} diff --git a/client/app/scripts/utils/topology-utils.js b/client/app/scripts/utils/topology-utils.js index 932f4f967a..2c6991164f 100644 --- a/client/app/scripts/utils/topology-utils.js +++ b/client/app/scripts/utils/topology-utils.js @@ -128,9 +128,9 @@ export function setTopologyUrlsById(topologyUrlsById, topologies) { export function filterHiddenTopologies(topologies, currentTopology) { currentTopology = currentTopology || makeMap(); - return topologies.filter(t => (!t.hide_if_empty || t.stats.node_count > 0 || - t.stats.filtered_nodes > 0 || t.id === currentTopology.get('id') || - t.id === currentTopology.get('parentId'))); + return topologies.filter(t => (!t.hide_if_empty || t.stats.node_count > 0 + || t.stats.filtered_nodes > 0 || t.id === currentTopology.get('id') + || t.id === currentTopology.get('parentId'))); } export function getCurrentTopologyOptions(state) { diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index dfbb2096d1..7a2b576345 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -4,25 +4,18 @@ import { defaults } from 'lodash'; import { Map as makeMap, List } from 'immutable'; import { - blurSearch, clearControlError, closeWebsocket, openWebsocket, receiveError, - receiveApiDetails, receiveNodesDelta, receiveNodeDetails, receiveControlError, - receiveControlNodeRemoved, receiveControlPipe, receiveControlPipeStatus, - receiveControlSuccess, receiveTopologies, receiveNotFound, - receiveNodesForTopology, receiveNodes, + receiveError, + receiveNodeDetails, + receiveNotFound, receiveNodesForTopology, receiveNodes, } from '../actions/app-actions'; -import { getCurrentTopologyUrl } from '../utils/topology-utils'; +import { getCurrentTopologyUrl } from './topology-utils'; import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; import { activeTopologyOptionsSelector } from '../selectors/topology'; -import { isPausedSelector } from '../selectors/time-travel'; - -import { API_REFRESH_INTERVAL, TOPOLOGY_REFRESH_INTERVAL } from '../constants/timer'; const log = debug('scope:web-api-utils'); -const reconnectTimerInterval = 5000; const updateFrequency = '5s'; -const FIRST_RENDER_TOO_LONG_THRESHOLD = 100; // ms const csrfToken = (() => { // Check for token at window level or parent level (for iframe); /* eslint-disable no-underscore-dangle */ @@ -38,16 +31,6 @@ const csrfToken = (() => { return token; })(); -let socket; -let reconnectTimer = 0; -let topologyTimer = 0; -let apiDetailsTimer = 0; -let controlErrorTimer = 0; -let currentUrl = null; -let createWebsocketAt = null; -let firstMessageOnWebsocketAt = null; -let continuePolling = true; - export function buildUrlQuery(params = makeMap(), state = null) { // Attach the time travel timestamp to every request to the backend. if (state) { @@ -105,7 +88,7 @@ export function getReportUrl(timestamp) { return `${getApiPath()}/api/report?${buildUrlQuery(makeMap({ timestamp }))}`; } -function topologiesUrl(state) { +export function topologiesUrl(state) { const activeTopologyOptions = activeTopologyOptionsSelector(state); const optionsQuery = buildUrlQuery(activeTopologyOptions, state); return `${getApiPath()}/api/topology?${optionsQuery}`; @@ -116,73 +99,17 @@ export function getWebsocketUrl(host = window.location.host, pathname = window.l return `${wsProto}://${host}${getApiPath(pathname)}`; } -function buildWebsocketUrl(topologyUrl, topologyOptions = makeMap(), state) { +export function buildWebsocketUrl(topologyUrl, topologyOptions = makeMap(), state) { topologyOptions = topologyOptions.set('t', updateFrequency); const optionsQuery = buildUrlQuery(topologyOptions, state); return `${getWebsocketUrl()}${topologyUrl}/ws?${optionsQuery}`; } -function createWebsocket(websocketUrl, getState, dispatch) { - if (socket) { - socket.onclose = null; - socket.onerror = null; - socket.close(); - // onclose() is not called, but that's fine since we're opening a new one - // right away - } - - // profiling - createWebsocketAt = new Date(); - firstMessageOnWebsocketAt = null; - - socket = new WebSocket(websocketUrl); - - socket.onopen = () => { - log(`Opening websocket to ${websocketUrl}`); - dispatch(openWebsocket()); - }; - - socket.onclose = () => { - clearTimeout(reconnectTimer); - log(`Closing websocket to ${websocketUrl}`, socket.readyState); - socket = null; - dispatch(closeWebsocket()); - - if (continuePolling && !isPausedSelector(getState())) { - reconnectTimer = setTimeout(() => { - createWebsocket(websocketUrl, getState, dispatch); - }, reconnectTimerInterval); - } - }; - - socket.onerror = () => { - log(`Error in websocket to ${websocketUrl}`); - dispatch(receiveError(websocketUrl)); - }; - - socket.onmessage = (event) => { - const msg = JSON.parse(event.data); - dispatch(receiveNodesDelta(msg)); - - // profiling (receiveNodesDelta triggers synchronous render) - if (!firstMessageOnWebsocketAt) { - firstMessageOnWebsocketAt = new Date(); - const timeToFirstMessage = firstMessageOnWebsocketAt - createWebsocketAt; - if (timeToFirstMessage > FIRST_RENDER_TOO_LONG_THRESHOLD) { - log( - 'Time (ms) to first nodes render after websocket was created', - firstMessageOnWebsocketAt - createWebsocketAt - ); - } - } - }; -} - /** * XHR wrapper. Applies a CSRF token (if it exists) and content-type to all requests. * Any opts that get passed in will override the defaults. */ -function doRequest(opts) { +export function doRequest(opts) { const config = defaults(opts, { contentType: 'application/json', type: 'json' @@ -212,7 +139,7 @@ function getNodesForTopologies(state, dispatch, topologyIds, topologyOptions = m ); } -function getNodesOnce(getState, dispatch) { +export function getNodesOnce(getState, dispatch) { const state = getState(); const topologyUrl = getCurrentTopologyUrl(state); const topologyOptions = activeTopologyOptionsSelector(state); @@ -248,63 +175,6 @@ export function getResourceViewNodesSnapshot(state, dispatch) { getNodesForTopologies(state, dispatch, topologyIds); } -function pollTopologies(getState, dispatch, initialPoll = false) { - // Used to resume polling when navigating between pages in Weave Cloud. - continuePolling = initialPoll === true ? true : continuePolling; - clearTimeout(topologyTimer); - // NOTE: getState is called every time to make sure the up-to-date state is used. - const url = topologiesUrl(getState()); - doRequest({ - error: (req) => { - log(`Error in topology request: ${req.responseText}`); - dispatch(receiveError(url)); - // Only retry in stand-alone mode - if (continuePolling && !isPausedSelector(getState())) { - topologyTimer = setTimeout(() => { - pollTopologies(getState, dispatch); - }, TOPOLOGY_REFRESH_INTERVAL); - } - }, - success: (res) => { - if (continuePolling && !isPausedSelector(getState())) { - dispatch(receiveTopologies(res)); - topologyTimer = setTimeout(() => { - pollTopologies(getState, dispatch); - }, TOPOLOGY_REFRESH_INTERVAL); - } - }, - url - }); -} - -function getTopologiesOnce(getState, dispatch) { - const url = topologiesUrl(getState()); - doRequest({ - error: (req) => { - log(`Error in topology request: ${req.responseText}`); - dispatch(receiveError(url)); - }, - success: (res) => { - dispatch(receiveTopologies(res)); - }, - url - }); -} - -function updateWebsocketChannel(getState, dispatch, forceRequest) { - const topologyUrl = getCurrentTopologyUrl(getState()); - const topologyOptions = activeTopologyOptionsSelector(getState()); - const websocketUrl = buildWebsocketUrl(topologyUrl, topologyOptions, getState()); - // Only recreate websocket if url changed or if forced (weave cloud instance reload); - const isNewUrl = websocketUrl !== currentUrl; - // `topologyUrl` can be undefined initially, so only create a socket if it is truthy - // and no socket exists, or if we get a new url. - if (topologyUrl && (!socket || isNewUrl || forceRequest)) { - createWebsocket(websocketUrl, getState, dispatch); - currentUrl = websocketUrl; - } -} - export function getNodeDetails(getState, dispatch) { const state = getState(); const nodeMap = state.get('nodeDetails'); @@ -351,91 +221,12 @@ export function getNodeDetails(getState, dispatch) { } } -export function getTopologies(getState, dispatch, forceRequest) { - if (isPausedSelector(getState())) { - getTopologiesOnce(getState, dispatch); - } else { - pollTopologies(getState, dispatch, forceRequest); - } -} - -export function getNodes(getState, dispatch, forceRequest = false) { - if (isPausedSelector(getState())) { - getNodesOnce(getState, dispatch); - } else { - updateWebsocketChannel(getState, dispatch, forceRequest); - } - getNodeDetails(getState, dispatch); -} - -export function getApiDetails(dispatch) { - clearTimeout(apiDetailsTimer); - const url = `${getApiPath()}/api`; - doRequest({ - error: (req) => { - log(`Error in api details request: ${req.responseText}`); - receiveError(url); - if (continuePolling) { - apiDetailsTimer = setTimeout(() => { - getApiDetails(dispatch); - }, API_REFRESH_INTERVAL / 2); - } - }, - success: (res) => { - dispatch(receiveApiDetails(res)); - if (continuePolling) { - apiDetailsTimer = setTimeout(() => { - getApiDetails(dispatch); - }, API_REFRESH_INTERVAL); - } - }, - url - }); -} - -export function doControlRequest(nodeId, control, dispatch) { - clearTimeout(controlErrorTimer); - const url = `${getApiPath()}/api/control/${encodeURIComponent(control.probeId)}/` - + `${encodeURIComponent(control.nodeId)}/${control.id}`; - doRequest({ - error: (err) => { - dispatch(receiveControlError(nodeId, err.response)); - controlErrorTimer = setTimeout(() => { - dispatch(clearControlError(nodeId)); - }, 10000); - }, - method: 'POST', - success: (res) => { - dispatch(receiveControlSuccess(nodeId)); - if (res) { - if (res.pipe) { - dispatch(blurSearch()); - const resizeTtyControl = res.resize_tty_control && - {id: res.resize_tty_control, nodeId: control.nodeId, probeId: control.probeId}; - dispatch(receiveControlPipe( - res.pipe, - nodeId, - res.raw_tty, - resizeTtyControl, - control - )); - } - if (res.removedNode) { - dispatch(receiveControlNodeRemoved(nodeId)); - } - } - }, - url - }); -} - - export function doResizeTty(pipeId, control, cols, rows) { const url = `${getApiPath()}/api/control/${encodeURIComponent(control.probeId)}/` + `${encodeURIComponent(control.nodeId)}/${control.id}`; return doRequest({ - data: JSON.stringify({height: rows.toString(), pipeID: pipeId, width: cols.toString()}), + data: JSON.stringify({ height: rows.toString(), pipeID: pipeId, width: cols.toString() }), method: 'POST', url, }) @@ -459,44 +250,3 @@ export function deletePipe(pipeId, dispatch) { url }); } - - -export function getPipeStatus(pipeId, dispatch) { - const url = `${getApiPath()}/api/pipe/${encodeURIComponent(pipeId)}/check`; - doRequest({ - complete: (res) => { - const status = { - 204: 'PIPE_ALIVE', - 404: 'PIPE_DELETED' - }[res.status]; - - if (!status) { - log('Unexpected pipe status:', res.status); - return; - } - - dispatch(receiveControlPipeStatus(pipeId, status)); - }, - method: 'GET', - url - }); -} - -export function stopPolling() { - clearTimeout(apiDetailsTimer); - clearTimeout(topologyTimer); - continuePolling = false; -} - -export function teardownWebsockets() { - clearTimeout(reconnectTimer); - if (socket) { - socket.onerror = null; - socket.onclose = null; - socket.onmessage = null; - socket.onopen = null; - socket.close(); - socket = null; - currentUrl = null; - } -} diff --git a/client/package.json b/client/package.json index 616d660a87..c9a3b8eac1 100644 --- a/client/package.json +++ b/client/package.json @@ -8,7 +8,7 @@ "main": "index.js", "dependencies": { "@babel/polyfill": "^7.0.0", - "babel-plugin-lodash": "3.3.2", + "babel-plugin-lodash": "3.3.4", "classnames": "2.2.5", "d3-array": "1.2.1", "d3-color": "1.0.3", @@ -56,19 +56,19 @@ "@babel/preset-react": "^7.0.0", "@fortawesome/fontawesome-free": "^5.5.0", "autoprefixer": "7.1.5", - "babel-eslint": "8.2.1", + "babel-eslint": "10.0.2", "babel-jest": "24.8.0", "babel-loader": "^8.0.0", "babel-plugin-transform-class-properties": "6.24.1", "babel-plugin-transform-object-rest-spread": "6.26.0", "clean-webpack-plugin": "0.1.17", "css-loader": "2.1.1", - "eslint": "5.16.0", - "eslint-config-airbnb": "16.1.0", - "eslint-loader": "2.1.2", - "eslint-plugin-import": "2.17.3", - "eslint-plugin-jsx-a11y": "6.2.1", - "eslint-plugin-react": "7.13.0", + "eslint": "6.1.0", + "eslint-config-airbnb": "17.1.1", + "eslint-loader": "2.2.1", + "eslint-plugin-import": "2.18.2", + "eslint-plugin-jsx-a11y": "6.2.3", + "eslint-plugin-react": "7.14.3", "express": "4.16.4", "file-loader": "1.1.11", "html-webpack-plugin": "3.2.0", @@ -130,6 +130,6 @@ ] }, "engines": { - "node": "^8.9.0" + "node": "^8.10.0" } } diff --git a/client/yarn.lock b/client/yarn.lock index 5ded5422fc..5b7d62de24 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -18,14 +18,6 @@ optionalDependencies: chokidar "^2.0.3" -"@babel/code-frame@7.0.0-beta.36": - version "7.0.0-beta.36" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.36.tgz#2349d7ec04b3a06945ae173280ef8579b63728e4" - dependencies: - chalk "^2.0.0" - esutils "^2.0.2" - js-tokens "^3.0.0" - "@babel/code-frame@7.0.0-beta.46": version "7.0.0-beta.46" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.46.tgz#e0d002100805daab1461c0fcb32a07e304f3a4f4" @@ -180,14 +172,6 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-function-name@7.0.0-beta.36": - version "7.0.0-beta.36" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.36.tgz#366e3bc35147721b69009f803907c4d53212e88d" - dependencies: - "@babel/helper-get-function-arity" "7.0.0-beta.36" - "@babel/template" "7.0.0-beta.36" - "@babel/types" "7.0.0-beta.36" - "@babel/helper-function-name@7.0.0-beta.46": version "7.0.0-beta.46" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.46.tgz#d0c4eed2e220e180f91b02e008dcc4594afe1d39" @@ -204,12 +188,6 @@ "@babel/template" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-get-function-arity@7.0.0-beta.36": - version "7.0.0-beta.36" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.36.tgz#f5383bac9a96b274828b10d98900e84ee43e32b8" - dependencies: - "@babel/types" "7.0.0-beta.36" - "@babel/helper-get-function-arity@7.0.0-beta.46": version "7.0.0-beta.46" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.46.tgz#7161bfe449b4183dbe25d1fe5579338b7429e5f2" @@ -234,9 +212,10 @@ dependencies: "@babel/types" "^7.0.0" -"@babel/helper-module-imports@^7.0.0": +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.0.0-beta.49": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d" + integrity sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A== dependencies: "@babel/types" "^7.0.0" @@ -735,14 +714,11 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/template@7.0.0-beta.36": - version "7.0.0-beta.36" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.36.tgz#02e903de5d68bd7899bce3c5b5447e59529abb00" +"@babel/runtime@^7.4.5": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12" dependencies: - "@babel/code-frame" "7.0.0-beta.36" - "@babel/types" "7.0.0-beta.36" - babylon "7.0.0-beta.36" - lodash "^4.2.0" + regenerator-runtime "^0.13.2" "@babel/template@7.0.0-beta.46": version "7.0.0-beta.46" @@ -769,19 +745,6 @@ "@babel/parser" "^7.2.2" "@babel/types" "^7.2.2" -"@babel/traverse@7.0.0-beta.36": - version "7.0.0-beta.36" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.36.tgz#1dc6f8750e89b6b979de5fe44aa993b1a2192261" - dependencies: - "@babel/code-frame" "7.0.0-beta.36" - "@babel/helper-function-name" "7.0.0-beta.36" - "@babel/types" "7.0.0-beta.36" - babylon "7.0.0-beta.36" - debug "^3.0.1" - globals "^11.1.0" - invariant "^2.2.0" - lodash "^4.2.0" - "@babel/traverse@7.0.0-beta.46", "@babel/traverse@^7.0.0-beta.42": version "7.0.0-beta.46" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.46.tgz#29a0c0395b3642f0297e6f8e475bde89f9343755" @@ -825,14 +788,6 @@ globals "^11.1.0" lodash "^4.17.11" -"@babel/types@7.0.0-beta.36": - version "7.0.0-beta.36" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.36.tgz#64f2004353de42adb72f9ebb4665fc35b5499d23" - dependencies: - esutils "^2.0.2" - lodash "^4.2.0" - to-fast-properties "^2.0.0" - "@babel/types@7.0.0-beta.46": version "7.0.0-beta.46" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.46.tgz#eb84399a699af9fcb244440cce78e1acbeb40e0c" @@ -849,6 +804,15 @@ lodash "^4.17.11" to-fast-properties "^2.0.0" +"@babel/types@^7.0.0-beta.49": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.0.tgz#e47d43840c2e7f9105bc4d3a2c371b4d0c7832ab" + integrity sha512-UFpDVqRABKsW01bvw7/wSUe56uy6RXM5+VJibVVAybDGxEW25jdwiFJEf7ASvSaC7sN7rbE/l3cLp2izav+CtQ== + dependencies: + esutils "^2.0.2" + lodash "^4.17.11" + to-fast-properties "^2.0.0" + "@babel/types@^7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.0.tgz#670724f77d24cce6cc7d8cf64599d511d164894c" @@ -1204,7 +1168,7 @@ ajv@^5.0.0, ajv@^5.2.3: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" -ajv@^6.1.0, ajv@^6.5.5, ajv@^6.9.1: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.5.5, ajv@^6.9.1: version "6.10.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" dependencies: @@ -1292,7 +1256,6 @@ argparse@^1.0.7: aria-query@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc" - integrity sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w= dependencies: ast-types-flow "0.0.7" commander "^2.11.0" @@ -1463,7 +1426,6 @@ aws4@^1.8.0: axobject-query@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" - integrity sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww== dependencies: ast-types-flow "0.0.7" @@ -1475,15 +1437,15 @@ babel-code-frame@^6.26.0: esutils "^2.0.2" js-tokens "^3.0.2" -babel-eslint@8.2.1: - version "8.2.1" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.1.tgz#136888f3c109edc65376c23ebf494f36a3e03951" +babel-eslint@10.0.2: + version "10.0.2" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.2.tgz#182d5ac204579ff0881684b040560fdcc1558456" dependencies: - "@babel/code-frame" "7.0.0-beta.36" - "@babel/traverse" "7.0.0-beta.36" - "@babel/types" "7.0.0-beta.36" - babylon "7.0.0-beta.36" - eslint-scope "~3.7.1" + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.0.0" + "@babel/traverse" "^7.0.0" + "@babel/types" "^7.0.0" + eslint-scope "3.7.1" eslint-visitor-keys "^1.0.0" babel-helper-function-name@^6.24.1: @@ -1503,13 +1465,6 @@ babel-helper-get-function-arity@^6.24.1: babel-runtime "^6.22.0" babel-types "^6.24.1" -babel-helper-module-imports@^7.0.0-beta.3: - version "7.0.0-beta.3" - resolved "https://registry.yarnpkg.com/babel-helper-module-imports/-/babel-helper-module-imports-7.0.0-beta.3.tgz#e15764e3af9c8e11810c09f78f498a2bdc71585a" - dependencies: - babel-types "7.0.0-beta.3" - lodash "^4.2.0" - babel-jest@24.8.0, babel-jest@^24.8.0: version "24.8.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.8.0.tgz#5c15ff2b28e20b0f45df43fe6b7f2aae93dba589" @@ -1551,14 +1506,15 @@ babel-plugin-jest-hoist@^24.6.0: dependencies: "@types/babel__traverse" "^7.0.6" -babel-plugin-lodash@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/babel-plugin-lodash/-/babel-plugin-lodash-3.3.2.tgz#da3a5b49ba27447f54463f6c4fa81396ccdd463f" +babel-plugin-lodash@3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/babel-plugin-lodash/-/babel-plugin-lodash-3.3.4.tgz#4f6844358a1340baed182adbeffa8df9967bc196" + integrity sha512-yDZLjK7TCkWl1gpBeBGmuaDIFhZKmkoL+Cu2MUUjv5VxUZx/z7tBGBCBcQs5RI1Bkz5LLmNdjx7paOyQtMovyg== dependencies: - babel-helper-module-imports "^7.0.0-beta.3" - babel-types "^6.26.0" + "@babel/helper-module-imports" "^7.0.0-beta.49" + "@babel/types" "^7.0.0-beta.49" glob "^7.1.1" - lodash "^4.17.4" + lodash "^4.17.10" require-package-name "^2.0.1" babel-plugin-syntax-class-properties@^6.8.0: @@ -1623,14 +1579,6 @@ babel-traverse@^6.24.1, babel-traverse@^6.26.0: invariant "^2.2.2" lodash "^4.17.4" -babel-types@7.0.0-beta.3: - version "7.0.0-beta.3" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-7.0.0-beta.3.tgz#cd927ca70e0ae8ab05f4aab83778cfb3e6eb20b4" - dependencies: - esutils "^2.0.2" - lodash "^4.2.0" - to-fast-properties "^2.0.0" - babel-types@^6.24.1, babel-types@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" @@ -1640,10 +1588,6 @@ babel-types@^6.24.1, babel-types@^6.26.0: lodash "^4.17.4" to-fast-properties "^1.0.3" -babylon@7.0.0-beta.36: - version "7.0.0-beta.36" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.36.tgz#3a3683ba6a9a1e02b0aa507c8e63435e39305b9e" - babylon@7.0.0-beta.46, babylon@^7.0.0-beta.42: version "7.0.0-beta.46" resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.46.tgz#b6ddaba81bbb130313932757ff9c195d527088b6" @@ -2296,6 +2240,11 @@ concat-stream@^1.5.0: readable-stream "^2.2.2" typedarray "^0.0.6" +confusing-browser-globals@^1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.7.tgz#5ae852bd541a910e7ffb2dbb864a2d21a36ad29b" + integrity sha512-cgHI1azax5ATrZ8rJ+ODDML9Fvu67PimB6aNxBrc/QwSaDaM9eTfIEUHx3bBLJJ82ioSb+/5zfsMCCEJax3ByQ== + console-browserify@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" @@ -2630,7 +2579,6 @@ dagre@0.8.4: damerau-levenshtein@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz#780cf7144eb2e8dbd1c3bb83ae31100ccc31a414" - integrity sha512-CBCRqFnpu715iPmw1KrdOrzRqbdFwQTwAWyyyYS42+iAgHCuXZ+/TdMgQkUENPomxEz9z1BEzuQU2Xw0kUuAgA== dashdash@^1.12.0: version "1.14.1" @@ -2656,7 +2604,7 @@ debug@2.6.9, debug@^2.1.1, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6. dependencies: ms "2.0.0" -debug@3.1.0, debug@^3.0.0, debug@^3.0.1, debug@^3.1.0: +debug@3.1.0, debug@^3.0.0, debug@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: @@ -2702,6 +2650,12 @@ define-properties@^1.1.2: foreach "^2.0.5" object-keys "^1.0.8" +define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + dependencies: + object-keys "^1.0.12" + define-property@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" @@ -2803,7 +2757,6 @@ doctrine@1.5.0, doctrine@^1.2.2: doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== dependencies: esutils "^2.0.2" @@ -2995,7 +2948,7 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.11.0, es-abstract@^1.5.1: +es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.5.1: version "1.13.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" dependencies: @@ -3113,29 +3066,35 @@ escope@^3.6.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-config-airbnb-base@^12.1.0: - version "12.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-12.1.0.tgz#386441e54a12ccd957b0a92564a4bafebd747944" +eslint-config-airbnb-base@^13.2.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-13.2.0.tgz#f6ea81459ff4dec2dda200c35f1d8f7419d57943" + integrity sha512-1mg/7eoB4AUeB0X1c/ho4vb2gYkNH8Trr/EgCT/aGmKhhG+F6vF5s8+iRBlWAzFIAphxIdp3YfEKgEl0f9Xg+w== dependencies: - eslint-restricted-globals "^0.1.1" + confusing-browser-globals "^1.0.5" + object.assign "^4.1.0" + object.entries "^1.1.0" -eslint-config-airbnb@16.1.0: - version "16.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-16.1.0.tgz#2546bfb02cc9fe92284bf1723ccf2e87bc45ca46" +eslint-config-airbnb@17.1.1: + version "17.1.1" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-17.1.1.tgz#2272e0b86bb1e2b138cdf88d07a3b6f4cda3d626" + integrity sha512-xCu//8a/aWqagKljt+1/qAM62BYZeNq04HmdevG5yUGWpja0I/xhqd6GdLRch5oetEGFiJAnvtGuTEAese53Qg== dependencies: - eslint-config-airbnb-base "^12.1.0" + eslint-config-airbnb-base "^13.2.0" + object.assign "^4.1.0" + object.entries "^1.1.0" eslint-import-resolver-node@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" - integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q== dependencies: debug "^2.6.9" resolve "^1.5.0" -eslint-loader@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-2.1.2.tgz#453542a1230d6ffac90e4e7cb9cadba9d851be68" +eslint-loader@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-2.2.1.tgz#28b9c12da54057af0845e2a6112701a2f6bf8337" + integrity sha512-RLgV9hoCVsMLvOxCuNjdqOrUqIj9oJg8hF44vzJaYqsAHuY9G2YAeN3joQ9nxP0p5Th9iFSIpKo+SD8KISxXRg== dependencies: loader-fs-cache "^1.0.0" loader-utils "^1.0.2" @@ -3146,15 +3105,14 @@ eslint-loader@2.1.2: eslint-module-utils@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.4.0.tgz#8b93499e9b00eab80ccb6614e69f03678e84e09a" - integrity sha512-14tltLm38Eu3zS+mt0KvILC3q8jyIAH518MlG+HO0p+yK885Lb1UHTY/UgR91eOyGdmxAPb+OLoW4znqIT6Ndw== dependencies: debug "^2.6.8" pkg-dir "^2.0.0" -eslint-plugin-import@2.17.3: - version "2.17.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.17.3.tgz#00548b4434c18faebaba04b24ae6198f280de189" - integrity sha512-qeVf/UwXFJbeyLbxuY8RgqDyEKCkqV7YC+E5S5uOjAp4tOc8zj01JP3ucoBM8JcEqd1qRasJSg6LLlisirfy0Q== +eslint-plugin-import@2.18.2: + version "2.18.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz#02f1180b90b077b33d447a17a2326ceb400aceb6" + integrity sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ== dependencies: array-includes "^3.0.3" contains-path "^0.1.0" @@ -3163,16 +3121,16 @@ eslint-plugin-import@2.17.3: eslint-import-resolver-node "^0.3.2" eslint-module-utils "^2.4.0" has "^1.0.3" - lodash "^4.17.11" minimatch "^3.0.4" + object.values "^1.1.0" read-pkg-up "^2.0.0" resolve "^1.11.0" -eslint-plugin-jsx-a11y@6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.1.tgz#4ebba9f339b600ff415ae4166e3e2e008831cf0c" - integrity sha512-cjN2ObWrRz0TTw7vEcGQrx+YltMvZoOEx4hWU8eEERDnBIU00OTq7Vr+jA7DFKxiwLNv4tTh5Pq2GUNEa8b6+w== +eslint-plugin-jsx-a11y@6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz#b872a09d5de51af70a97db1eea7dc933043708aa" dependencies: + "@babel/runtime" "^7.4.5" aria-query "^3.0.0" array-includes "^3.0.3" ast-types-flow "^0.0.7" @@ -3180,42 +3138,34 @@ eslint-plugin-jsx-a11y@6.2.1: damerau-levenshtein "^1.0.4" emoji-regex "^7.0.2" has "^1.0.3" - jsx-ast-utils "^2.0.1" + jsx-ast-utils "^2.2.1" -eslint-plugin-react@7.13.0: - version "7.13.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.13.0.tgz#bc13fd7101de67996ea51b33873cd9dc2b7e5758" - integrity sha512-uA5LrHylu8lW/eAH3bEQe9YdzpPaFd9yAJTwTi/i/BKTD7j6aQMKVAdGM/ML72zD6womuSK7EiGtMKuK06lWjQ== +eslint-plugin-react@7.14.3: + version "7.14.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.3.tgz#911030dd7e98ba49e1b2208599571846a66bdf13" + integrity sha512-EzdyyBWC4Uz2hPYBiEJrKCUi2Fn+BJ9B/pJQcjw5X+x/H2Nm59S4MJIvL4O5NEE0+WbnQwEBxWY03oUk+Bc3FA== dependencies: array-includes "^3.0.3" doctrine "^2.1.0" has "^1.0.3" jsx-ast-utils "^2.1.0" + object.entries "^1.1.0" object.fromentries "^2.0.0" + object.values "^1.1.0" prop-types "^15.7.2" resolve "^1.10.1" -eslint-restricted-globals@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7" - -eslint-scope@^3.7.1: +eslint-scope@3.7.1, eslint-scope@^3.7.1: version "3.7.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" dependencies: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-scope@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-scope@~3.7.1: - version "3.7.3" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.3.tgz#bb507200d3d17f60247636160b4826284b108535" +eslint-scope@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" + integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== dependencies: esrecurse "^4.1.0" estraverse "^4.1.1" @@ -3228,46 +3178,48 @@ eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" -eslint@5.16.0: - version "5.16.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea" +eslint@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.1.0.tgz#06438a4a278b1d84fb107d24eaaa35471986e646" + integrity sha512-QhrbdRD7ofuV09IuE2ySWBz0FyXCq0rriLTZXZqaWSI79CVtHVRdkFuFTViiqzZhkCgfOh9USpriuGN2gIpZDQ== dependencies: "@babel/code-frame" "^7.0.0" - ajv "^6.9.1" + ajv "^6.10.0" chalk "^2.1.0" cross-spawn "^6.0.5" debug "^4.0.1" doctrine "^3.0.0" - eslint-scope "^4.0.3" + eslint-scope "^5.0.0" eslint-utils "^1.3.1" eslint-visitor-keys "^1.0.0" - espree "^5.0.1" + espree "^6.0.0" esquery "^1.0.1" esutils "^2.0.2" file-entry-cache "^5.0.1" functional-red-black-tree "^1.0.1" - glob "^7.1.2" + glob-parent "^5.0.0" globals "^11.7.0" ignore "^4.0.6" import-fresh "^3.0.0" imurmurhash "^0.1.4" - inquirer "^6.2.2" - js-yaml "^3.13.0" + inquirer "^6.4.1" + is-glob "^4.0.0" + js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.3.0" - lodash "^4.17.11" + lodash "^4.17.14" minimatch "^3.0.4" mkdirp "^0.5.1" natural-compare "^1.4.0" optionator "^0.8.2" - path-is-inside "^1.0.2" progress "^2.0.0" regexpp "^2.0.1" - semver "^5.5.1" - strip-ansi "^4.0.0" - strip-json-comments "^2.0.1" + semver "^6.1.2" + strip-ansi "^5.2.0" + strip-json-comments "^3.0.1" table "^5.2.3" text-table "^0.2.0" + v8-compile-cache "^2.0.3" eslint@^2.7.0: version "2.13.1" @@ -3314,9 +3266,9 @@ espree@^3.1.6: acorn "^5.5.0" acorn-jsx "^3.0.0" -espree@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.1.tgz#5d6526fa4fc7f0788a5cf75b15f30323e2f81f7a" +espree@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.0.0.tgz#716fc1f5a245ef5b9a7fdb1d7b0d3f02322e75f6" dependencies: acorn "^6.0.7" acorn-jsx "^5.0.0" @@ -3960,6 +3912,13 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" +glob-parent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.0.0.tgz#1dc99f0f39b006d3e92c2c284068382f0c20e954" + integrity sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg== + dependencies: + is-glob "^4.0.1" + glob-to-regexp@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" @@ -4525,9 +4484,10 @@ inquirer@^0.12.0: strip-ansi "^3.0.0" through "^2.3.6" -inquirer@^6.2.2: - version "6.3.1" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.3.1.tgz#7a413b5e7950811013a3db491c61d1f3b776e8e7" +inquirer@^6.4.1: + version "6.5.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.0.tgz#2303317efc9a4ea7ec2e2df6f86569b734accf42" + integrity sha512-scfHejeG/lVZSpvCXpsB4j/wQNPM5JC8kiElOI0OUTwmc1RTpXr4H32/HOlQHcZiYl2z2VElwuCVDRG8vFmbnA== dependencies: ansi-escapes "^3.2.0" chalk "^2.4.2" @@ -4535,7 +4495,7 @@ inquirer@^6.2.2: cli-width "^2.0.0" external-editor "^3.0.3" figures "^2.0.0" - lodash "^4.17.11" + lodash "^4.17.12" mute-stream "0.0.7" run-async "^2.2.0" rxjs "^6.4.0" @@ -4750,6 +4710,13 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" +is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + is-hexadecimal@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.1.tgz#6e084bbc92061fbb0971ec58b6ce6d404e24da69" @@ -5311,7 +5278,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" -js-yaml@^3.13.0, js-yaml@^3.13.1: +js-yaml@^3.13.1: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" dependencies: @@ -5441,19 +5408,19 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jsx-ast-utils@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f" - dependencies: - array-includes "^3.0.3" - jsx-ast-utils@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.1.0.tgz#0ee4e2c971fb9601c67b5641b71be80faecf0b36" - integrity sha512-yDGDG2DS4JcqhA6blsuYbtsT09xL8AoLuUR2Gb5exrw7UEM19sBcOTq+YBBhrNbl0PUC4R4LnFu+dHg2HKeVvA== dependencies: array-includes "^3.0.3" +jsx-ast-utils@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.1.tgz#4d4973ebf8b9d2837ee91a8208cc66f3a2776cfb" + dependencies: + array-includes "^3.0.3" + object.assign "^4.1.0" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -5647,6 +5614,11 @@ lodash@^4.0.0, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lod version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" +lodash@^4.17.12, lodash@^4.17.14: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + log-symbols@^2.0.0, log-symbols@^2.1.0, log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" @@ -6372,10 +6344,18 @@ object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" +object.entries@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519" + dependencies: + define-properties "^1.1.3" + es-abstract "^1.12.0" + function-bind "^1.1.1" + has "^1.0.3" + object.fromentries@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.0.tgz#49a543d92151f8277b3ac9600f1e930b189d30ab" - integrity sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA== dependencies: define-properties "^1.1.2" es-abstract "^1.11.0" @@ -6402,6 +6382,15 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" +object.values@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9" + dependencies: + define-properties "^1.1.3" + es-abstract "^1.12.0" + function-bind "^1.1.1" + has "^1.0.3" + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -6642,7 +6631,7 @@ path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" -path-is-inside@^1.0.1, path-is-inside@^1.0.2: +path-is-inside@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" @@ -7521,6 +7510,10 @@ regenerator-runtime@^0.12.0: version "0.12.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" +regenerator-runtime@^0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447" + regenerator-transform@^0.13.4: version "0.13.4" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb" @@ -8021,9 +8014,10 @@ semver@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" -semver@^5.5.1: - version "5.7.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" +semver@^6.1.2: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== semver@~5.3.0: version "5.3.0" @@ -8467,14 +8461,19 @@ strip-indent@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" -strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" +strip-json-comments@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" + integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== strip-json-comments@~1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + style-loader@0.21.0: version "0.21.0" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.21.0.tgz#68c52e5eb2afc9ca92b6274be277ee59aea3a852" @@ -9181,6 +9180,11 @@ v8-compile-cache@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c" +v8-compile-cache@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" + integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w== + validate-npm-package-license@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"