Merge pull request #3643 from weaveworks/security-updates

Update to newer eslint
This commit is contained in:
Filip Barl
2019-08-12 12:15:03 +02:00
committed by GitHub
67 changed files with 1414 additions and 1152 deletions
+1
View File
@@ -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,
+1 -1
View File
@@ -1 +1 @@
v8.9.0
v8.10.0
+2 -332
View File
@@ -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,
@@ -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);
};
}
+5 -6
View File
@@ -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
)
}
</Motion>
);
+2 -1
View File
@@ -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';
@@ -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) {
+34 -21
View File
@@ -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({
<Icon color={getNodeColor(rank, label)} />
</div>
<div className="truncate">
{label} {showSubLabel && <span className="nodes-grid-label-minor">{labelMinor}</span>}
{label}
{' '}
{showSubLabel && <span className="nodes-grid-label-minor">{labelMinor}</span>}
</div>
</div>
);
@@ -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 (
<div className="nodes-grid" ref={this.saveTableRef}>
{nodes.size > 0 && <NodeDetailsTable
style={cmpStyle}
className={className}
renderIdCell={renderIdCell}
tbodyStyle={tbodyStyle}
topologyId={this.props.currentTopologyId}
onSortChange={this.onSortChange}
onClickRow={this.onClickRow}
sortedBy={gridSortedBy}
sortedDesc={gridSortedDesc}
selectedNodeId={this.props.selectedNodeId}
limit={1000}
{...detailsData}
/>}
{nodes.size > 0 && (
<NodeDetailsTable
style={cmpStyle}
className={className}
renderIdCell={renderIdCell}
tbodyStyle={tbodyStyle}
topologyId={this.props.currentTopologyId}
onSortChange={this.onSortChange}
onClickRow={this.onClickRow}
sortedBy={gridSortedBy}
sortedDesc={gridSortedDesc}
selectedNodeId={this.props.selectedNodeId}
limit={1000}
{...detailsData}
/>
)}
</div>
);
}
+1 -1
View File
@@ -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;
+16 -9
View File
@@ -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 && <TroubleshootingMenu />}
{showingDetails && <Details
{showingDetails && (
<Details
renderNodeDetailsExtras={this.props.renderNodeDetailsExtras}
/>}
/>
)}
<div className="header">
{timeTravelSupported && this.props.renderTimeTravel()}
<div className="selectors">
<div className="logo">
{!isIframe &&
{!isIframe
&& (
<svg width="100%" height="100%" viewBox="0 0 1089 217">
<Logo />
</svg>
)
}
</div>
<Search />
@@ -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);
+55 -39
View File
@@ -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 {
<div className="debug-panel">
<div>
<strong>Add nodes </strong>
<button onClick={() => this.addNodes(1)}>+1</button>
<button onClick={() => this.addNodes(10)}>+10</button>
<button type="button" onClick={() => this.addNodes(1)}>+1</button>
<button type="button" onClick={() => this.addNodes(10)}>+10</button>
<input type="number" onChange={this.onChange} value={this.state.nodesToAdd} />
<button onClick={() => this.addNodes(this.state.nodesToAdd)}>+</button>
<button onClick={() => this.asyncDispatch(addAllVariants)}>Variants</button>
<button onClick={() => this.asyncDispatch(addAllMetricVariants(availableMetrics))}>
<button type="button" onClick={() => this.addNodes(this.state.nodesToAdd)}>+</button>
<button type="button" onClick={() => this.asyncDispatch(addAllVariants)}>
Variants
</button>
<button
type="button"
onClick={() => this.asyncDispatch(addAllMetricVariants(availableMetrics))}>
Metric Variants
</button>
<button onClick={() => this.addNodes(1, LOREM)}>Long name</button>
<button onClick={() => this.addInternetNode()}>Internet</button>
<button onClick={() => this.removeNode()}>Remove node</button>
<button onClick={() => this.updateAdjacencies()}>Update adj.</button>
<button type="button" onClick={() => this.addNodes(1, LOREM)}>Long name</button>
<button type="button" onClick={() => this.addInternetNode()}>Internet</button>
<button type="button" onClick={() => this.removeNode()}>Remove node</button>
<button type="button" onClick={() => this.updateAdjacencies()}>Update adj.</button>
</div>
<div>
<strong>Logging </strong>
<button onClick={() => enableLog('*')}>scope:*</button>
<button onClick={() => enableLog('dispatcher')}>scope:dispatcher</button>
<button onClick={() => enableLog('app-key-press')}>scope:app-key-press</button>
<button onClick={() => enableLog('terminal')}>scope:terminal</button>
<button onClick={() => disableLog()}>Disable log</button>
<button type="button" onClick={() => enableLog('*')}>scope:*</button>
<button type="button" onClick={() => enableLog('dispatcher')}>scope:dispatcher</button>
<button type="button" onClick={() => enableLog('app-key-press')}>
scope:app-key-press
</button>
<button type="button" onClick={() => enableLog('terminal')}>scope:terminal</button>
<button type="button" onClick={() => disableLog()}>Disable log</button>
</div>
<div>
<strong>Colors </strong>
<button onClick={this.toggleColors}>toggle</button>
<button type="button" onClick={this.toggleColors}>toggle</button>
</div>
{this.state.showColors &&
<table>
<tbody>
{LABEL_PREFIXES.map(r => (
<tr key={r}>
<td
title={`${r}`}
style={{backgroundColor: hsl(text2degree(r), 0.5, 0.5).toString()}} />
</tr>
))}
</tbody>
</table>}
{this.state.showColors
&& (
<table>
<tbody>
{LABEL_PREFIXES.map(r => (
<tr key={r}>
<td
title={`${r}`}
style={{ backgroundColor: hsl(text2degree(r), 0.5, 0.5).toString() }} />
</tr>
))}
</tbody>
</table>
)}
{this.state.showColors && [getNodeColor, getNodeColorDark].map(fn => (
<table key={fn}>
@@ -330,7 +340,7 @@ class DebugToolbar extends React.Component {
{LABEL_PREFIXES.map(r => (
<tr key={r}>
{LABEL_PREFIXES.map(c => (
<td key={c} title={`(${r}, ${c})`} style={{backgroundColor: fn(r, c)}} />
<td key={c} title={`(${r}, ${c})`} style={{ backgroundColor: fn(r, c) }} />
))}
</tr>
))}
@@ -340,14 +350,20 @@ class DebugToolbar extends React.Component {
<div>
<strong>State </strong>
<button onClick={() => this.setLoading(true)}>Set doing initial load</button>
<button onClick={() => this.setLoading(false)}>Stop</button>
<button type="button" onClick={() => this.setLoading(true)}>
Set doing initial load
</button>
<button type="button" onClick={() => this.setLoading(false)}>Stop</button>
</div>
<div>
<strong>Short-lived nodes </strong>
<button onClick={() => this.setShortLived()}>Toggle short-lived nodes</button>
<button onClick={() => this.setIntermittent()}>Toggle intermittent nodes</button>
<button type="button" onClick={() => this.setShortLived()}>
Toggle short-lived nodes
</button>
<button type="button" onClick={() => this.setIntermittent()}>
Toggle intermittent nodes
</button>
</div>
</div>
);
@@ -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 {
<div
onTransitionEnd={this.handleTransitionEnd}
className="terminal-animation-wrapper"
style={{transform: this.getTransform()}} >
style={{transform: this.getTransform()}}>
<Terminal
key={pipe.get('id')}
pipe={pipe}
+25 -11
View File
@@ -42,15 +42,19 @@ class Footer extends React.Component {
return (
<div className="footer">
<div className="footer-status">
{versionUpdate &&
<a
className="footer-versionupdate"
title={versionUpdateTitle}
href={versionUpdate.get('downloadUrl')}
target="_blank"
rel="noopener noreferrer">
Update available: {versionUpdate.get('version')}
</a>
{versionUpdate
&& (
<a
className="footer-versionupdate"
title={versionUpdateTitle}
href={versionUpdate.get('downloadUrl')}
target="_blank"
rel="noopener noreferrer">
Update available:
{' '}
{versionUpdate.get('version')}
</a>
)
}
<span className="footer-label">Version</span>
{version || '...'}
@@ -64,15 +68,21 @@ class Footer extends React.Component {
<div className="footer-tools">
<button
type="button"
className="footer-icon"
onClick={this.handleRelayoutClick}
title={forceRelayoutTitle}>
<i className="fa fa-sync" />
</button>
<button onClick={this.handleContrastClick} className="footer-icon" title={otherContrastModeTitle}>
<button
type="button"
onClick={this.handleContrastClick}
className="footer-icon"
title={otherContrastModeTitle}>
<i className="fa fa-adjust" />
</button>
<button
type="button"
onClick={this.props.toggleTroubleshootingMenu}
className="footer-icon"
title="Open troubleshooting menu"
@@ -80,7 +90,11 @@ class Footer extends React.Component {
>
<i className="fa fa-bug" />
</button>
<button className="footer-icon" onClick={this.props.toggleHelp} title="Show help">
<button
type="button"
className="footer-icon"
onClick={this.props.toggleHelp}
title="Show help">
<i className="fa fa-question" />
</button>
</div>
+54 -20
View File
@@ -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 (
<div>
{cuts.map(({key, label}) => (
{cuts.map(({ key, label }) => (
<div key={key} className="help-panel-shortcuts-shortcut">
<div className="key"><kbd>{key}</kbd></div>
<div className="label">{label}</div>
@@ -51,9 +51,16 @@ function renderShortcutPanel() {
const BASIC_SEARCHES = [
{label: 'All fields for foo', term: 'foo'},
{ label: 'All fields for foo', term: 'foo' },
{
label: <span>Any field matching <b>pid</b> for the value 12345</span>,
label: (
<span>
Any field matching
<b>pid</b>
{' '}
for the value 12345
</span>
),
term: 'pid: 12345'
},
];
@@ -65,16 +72,37 @@ const REGEX_SEARCHES = [
term: 'foo|bar'
},
{
label: <span><b>command</b> field for foobar or foobaz</span>,
label: (
<span>
<b>command</b>
{' '}
field for foobar or foobaz
</span>
),
term: 'command: foo(bar|baz)'
},
];
const METRIC_SEARCHES = [
{label: <span><b>CPU</b> greater than 4%</span>, term: 'cpu > 4%'},
{
label: <span><b>Memory</b> less than 10 megabytes</span>,
label: (
<span>
<b>CPU</b>
{' '}
greater than 4%
</span>
),
term: 'cpu > 4%'
},
{
label: (
<span>
<b>Memory</b>
{' '}
less than 10 megabytes
</span>
),
term: 'memory < 10mb'
},
];
@@ -83,7 +111,7 @@ const METRIC_SEARCHES = [
function renderSearches(searches) {
return (
<div>
{searches.map(({term, label}) => (
{searches.map(({ term, label }) => (
<div key={term} className="help-panel-search-row">
<div className="help-panel-search-row-term">
<i className="fa fa-search search-label-icon" />
@@ -117,7 +145,7 @@ function renderSearchPanel() {
function renderFieldsPanel(currentTopologyName, searchableFields) {
const none = (
<span style={{fontStyle: 'italic'}}>None</span>
<span style={{ fontStyle: 'italic' }}>None</span>
);
const currentTopology = (
<span className="help-panel-fields-current-topology">
@@ -129,8 +157,14 @@ function renderFieldsPanel(currentTopologyName, searchableFields) {
<div className="help-panel-fields">
<h2>Fields and Metrics</h2>
<p>
Searchable fields and metrics in the <br />
currently selected {currentTopology} topology:
Searchable fields and metrics in the
{' '}
<br />
currently selected
{' '}
{currentTopology}
{' '}
topology:
</p>
<div className="help-panel-fields-fields">
<div className="help-panel-fields-fields-column">
@@ -162,7 +196,7 @@ function HelpPanel({
}) {
return (
<div className="help-panel-wrapper">
<div className="help-panel" style={{marginTop: canvasMargins.top}}>
<div className="help-panel" style={{ marginTop: canvasMargins.top }}>
<div className="help-panel-header">
<h2>Help</h2>
</div>
@@ -7,7 +7,8 @@ const Match = (searchTerms, match) => (
<div className="matched-results-match" key={match.label}>
<div className="matched-results-match-wrapper">
<span className="matched-results-match-label">
{match.label}:
{match.label}
:
</span>
<MatchedText
text={match.text}
@@ -42,10 +43,12 @@ export default class MatchedResults extends React.PureComponent {
.take(SHOW_ROW_COUNT)
.map(fieldId => Match(searchTerms, matches.get(fieldId)))
}
{moreFieldMatches &&
{moreFieldMatches
&& (
<div className="matched-results-more" title={moreFieldMatchesTitle}>
{`${moreFieldMatches.size} more matches`}
</div>
)
}
</div>
);
@@ -22,7 +22,8 @@ class MetricSelector extends React.Component {
return (
<div className="metric-selector">
{hasMetrics &&
{hasMetrics
&& (
<div className="metric-selector-wrapper" onMouseLeave={this.onMouseOut}>
{availableMetrics.map(metric => (
<MetricSelectorItem
@@ -31,6 +32,7 @@ class MetricSelector extends React.Component {
/>
))}
</div>
)
}
</div>
);
+32 -15
View File
@@ -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 (
<div className="node-details-tools-wrapper">
<div className="node-details-tools">
{showSwitchTopology &&
{showSwitchTopology
&& (
<i
title={topologyTitle}
className="fa fa-long-arrow-alt-left"
onClick={this.handleShowTopologyForNode}>
<span>Show in <span>{this.props.topologyId.replace(/-/g, ' ')}</span></span>
<span>
Show in
<span>{this.props.topologyId.replace(/-/g, ' ')}</span>
</span>
</i>
)
}
<i
title="Close details"
@@ -81,9 +86,9 @@ class NodeDetails extends React.Component {
// mounted, the spinner is displayed blurred the whole time in Chrome (possibly
// caused by a bug having to do with animating the details panel).
const spinnerClassName = classNames('fa fa-circle-notch', { 'fa-spin': this.props.mounted });
const nodeColor = (node ?
getNodeColorDark(node.get('rank'), label, node.get('pseudo')) :
getNeutralColor());
const nodeColor = (node
? getNodeColorDark(node.get('rank'), label, node.get('pseudo'))
: getNeutralColor());
const tools = this.renderTools();
const styles = {
header: {
@@ -130,7 +135,9 @@ class NodeDetails extends React.Component {
</div>
<div className="node-details-content">
<p className="node-details-content-info">
<strong>{this.props.label}</strong> not found!
<strong>{this.props.label}</strong>
{' '}
not found!
</p>
</div>
<Overlay faded={this.props.transitioning} />
@@ -176,14 +183,17 @@ class NodeDetails extends React.Component {
<MatchedText text={details.label} match={nodeMatches.get('label')} />
</h2>
<div className="node-details-header-relatives">
{details.parents && <NodeDetailsRelatives
{details.parents && (
<NodeDetailsRelatives
matches={nodeMatches.get('parents')}
relatives={details.parents} />}
relatives={details.parents} />
)}
</div>
</div>
</div>
{showControls &&
{showControls
&& (
<div className="tour-step-anchor node-details-controls-wrapper" style={styles.controls}>
<NodeDetailsControls
nodeId={this.props.nodeId}
@@ -191,10 +201,12 @@ class NodeDetails extends React.Component {
pending={pending}
error={error} />
</div>
)
}
<div className="node-details-content">
{details.metrics &&
{details.metrics
&& (
<div className="node-details-content-section">
<div className="node-details-content-section-header">Status</div>
<NodeDetailsHealth
@@ -202,12 +214,15 @@ class NodeDetails extends React.Component {
topologyId={topologyId}
/>
</div>
)
}
{details.metadata &&
{details.metadata
&& (
<div className="node-details-content-section">
<div className="node-details-content-section-header">Info</div>
<NodeDetailsInfo rows={details.metadata} matches={nodeMatches.get('metadata')} />
</div>
)
}
{details.connections && details.connections.filter(cs => cs.connections.length > 0)
@@ -219,7 +234,7 @@ class NodeDetails extends React.Component {
nodeIdKey="nodeId"
/>
</div>
))}
))}
{details.children && details.children.map(children => (
<div className="node-details-content-section" key={children.topologyId}>
@@ -233,11 +248,13 @@ class NodeDetails extends React.Component {
<div className="node-details-content-section" key={table.id}>
<div className="node-details-content-section-header">
{table.label && table.label.length > 0 && table.label}
{table.truncationCount > 0 &&
{table.truncationCount > 0
&& (
<span
className="node-details-content-section-header-warning">
<Warning text={getTruncationText(table.truncationCount)} />
</span>
)
}
</div>
{this.renderTable(table)}
@@ -266,7 +283,7 @@ class NodeDetails extends React.Component {
matches={nodeMatches.get('tables')}
/>
);
} else if (isPropertyList(table)) {
} if (isPropertyList(table)) {
return (
<NodeDetailsPropertyList
rows={table.rows}
@@ -70,8 +70,9 @@ describe('NodeDetailsTable', () => {
// (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}
/>
/>
</Provider>
));
@@ -127,7 +128,7 @@ describe('NodeDetailsTable', () => {
sortedBy="kubernetes_namespace"
nodeIdKey="id"
nodes={nodes}
/>
/>
</Provider>
));
@@ -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) {
@@ -15,11 +15,13 @@ export default function NodeDetailsControls({
return (
<div className="node-details-controls">
{error &&
{error
&& (
<div className="node-details-controls-error" title={error}>
<i className="node-details-controls-error-icon fa fa-exclamation-triangle" />
<span className="node-details-controls-error-messages">{error}</span>
</div>
)
}
<span className="node-details-controls-buttons">
{sortBy(controls, 'rank').map(control => (
@@ -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' ?
<a
rel="noopener noreferrer"
target="_blank"
className="node-details-table-node-link"
href={value}>
{value}
</a> :
<MatchedText text={value} match={match} />
{column.dataType === 'link'
? (
<a
rel="noopener noreferrer"
target="_blank"
className="node-details-table-node-link"
href={value}>
{value}
</a>
)
: <MatchedText text={value} match={match} />
}
</td>
);
@@ -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 (
<div className="node-details-health" style={{ justifyContent: 'space-around' }}>
<div className="node-details-health-wrapper">
{shownWithData.map(item => (<NodeDetailsHealthLinkItem
{...item}
key={item.id}
topologyId={topologyId}
/>))}
{shownWithData.map(item => (
<NodeDetailsHealthLinkItem
{...item}
key={item.id}
topologyId={topologyId}
/>
))}
</div>
<div className="node-details-health-wrapper">
{shownEmpty.map(item => (<NodeDetailsHealthLinkItem
{...item}
key={item.id}
topologyId={topologyId}
/>))}
{shownEmpty.map(item => (
<NodeDetailsHealthLinkItem
{...item}
key={item.id}
topologyId={topologyId}
/>
))}
</div>
<ShowMore
handleClick={this.handleClickMore}
@@ -17,8 +17,9 @@ class NodeDetailsInfo extends React.Component {
}
handleClickMore() {
const expanded = !this.state.expanded;
this.setState({expanded});
this.setState(prevState => ({
expanded: !prevState.expanded
}));
}
render() {
@@ -47,18 +48,22 @@ class NodeDetailsInfo extends React.Component {
{field.label}
</div>
<div className="node-details-info-field-value truncate" title={title}>
{field.dataType === 'link' ?
<a
rel="noopener noreferrer"
target="_blank"
className="truncate node-details-table-node-link"
href={value}>
{value}
</a> :
<MatchedText
text={value}
truncate={field.truncate}
match={matches.get(field.id)} />
{field.dataType === 'link'
? (
<a
rel="noopener noreferrer"
target="_blank"
className="truncate node-details-table-node-link"
href={value}>
{value}
</a>
)
: (
<MatchedText
text={value}
truncate={field.truncate}
match={matches.get(field.id)} />
)
}
</div>
</div>
@@ -9,10 +9,12 @@ import ShowMore from '../show-more';
const Controls = controls => (
<div className="node-details-property-list-controls">
{sortBy(controls, 'rank').map(control => (<NodeDetailsControlButton
nodeId={control.nodeId}
control={control}
key={control.id} />))}
{sortBy(controls, 'rank').map(control => (
<NodeDetailsControlButton
nodeId={control.nodeId}
control={control}
key={control.id} />
))}
</div>
);
@@ -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() {
@@ -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';
@@ -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 {
<NodeDetailsRelativesLink
key={relative.id}
match={matches.get(relative.id)}
{...relative} />))}
{showLimitAction &&
<span
className="node-details-relatives-more"
onClick={this.handleLimitClick}>
{limitActionText}
</span>
{...relative} />
))}
{showLimitAction
&& (
<span
className="node-details-relatives-more"
onClick={this.handleLimitClick}>
{limitActionText}
</span>
)
}
</div>
);
@@ -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 (
<td className={headerClasses.join(' ')} style={style} title={header.label} key={header.id}>
@@ -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 {
@@ -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) {
@@ -54,14 +54,17 @@ function renderValues(node, columns = [], columnStyles = [], timestamp = null, t
title={title}
style={style}
key={field.id}>
{field.dataType === 'link' ?
<a
rel="noopener noreferrer"
target="_blank"
className="node-details-table-node-link"
href={value}>{value}
</a> :
value}
{field.dataType === 'link'
? (
<a
rel="noopener noreferrer"
target="_blank"
className="node-details-table-node-link"
href={value}>
{value}
</a>
)
: value}
</td>
);
}
@@ -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 =>
(<NodeDetailsTableNodeLink
{intersperse(field.relatives.map(relative => (
<NodeDetailsTableNodeLink
key={relative.id}
linkable
nodeId={relative.id}
{...relative}
/>)), ' ')}
/>
)), ' ')}
</td>
);
}
@@ -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}>
<td className="node-details-table-node-label truncate" style={firstColumnStyle}>
{this.props.renderIdCell(Object.assign(node, {nodeId, topologyId}))}
{this.props.renderIdCell(Object.assign(node, { nodeId, topologyId }))}
</td>
{values}
</tr>
@@ -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 <tr className="min-height-constraint" style={{height}} />;
return <tr className="min-height-constraint" style={{ height }} />;
}
@@ -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 {
<div className="node-details-table-wrapper">
<table className="node-details-table">
<thead ref={this.saveTableHeadRef}>
{this.props.nodes && this.props.nodes.length > 0 && <NodeDetailsTableHeaders
headers={headers}
sortedBy={sortedBy}
sortedDesc={sortedDesc}
onClick={this.updateSorted}
/>}
{this.props.nodes && this.props.nodes.length > 0 && (
<NodeDetailsTableHeaders
headers={headers}
sortedBy={sortedBy}
sortedDesc={sortedDesc}
onClick={this.updateSorted}
/>
)}
</thead>
<tbody
style={this.props.tbodyStyle}
@@ -308,7 +313,7 @@ NodeDetailsTable.defaultProps = {
limit: NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT,
// key to identify a node in a row (used for topology links)
nodeIdKey: 'id',
onSortChange: () => {},
onSortChange: () => { },
sortedBy: null,
sortedDesc: null,
};
@@ -35,11 +35,13 @@ class NodesResourcesLayer extends React.Component {
/>
))}
</g>
{!layoutNodes.isEmpty() && <NodeResourcesLayerTopology
{!layoutNodes.isEmpty() && (
<NodeResourcesLayerTopology
verticalPosition={layerVerticalPosition}
transform={transform}
topologyId={topologyId}
/>}
/>
)}
</g>
);
}
@@ -13,11 +13,21 @@ export default class NodeResourcesMetricBoxInfo extends React.Component {
<span>
<strong>
{showExtendedInfo ? humanizedRelativeConsumption : humanizedAbsoluteConsumption}
</strong> used
{showExtendedInfo &&
</strong>
{' '}
used
{showExtendedInfo
&& (
<i>
{' - '}({humanizedAbsoluteConsumption} / <strong>{humanizedTotalCapacity}</strong>)
{' - '}
(
{humanizedAbsoluteConsumption}
{' '}
/
<strong>{humanizedTotalCapacity}</strong>
)
</i>
)
}
</span>
);
@@ -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 (
<g
@@ -123,13 +123,25 @@ class NodeResourcesMetricBox extends React.Component {
onClick={this.handleClick}
ref={this.saveNodeRef}
>
<title>{label} - {type} usage at {resourceUsageTooltipInfo}</title>
{showCapacity && <rect
<title>
{label}
{' '}
-
{' '}
{type}
{' '}
usage at
{' '}
{resourceUsageTooltipInfo}
</title>
{showCapacity && (
<rect
className="frame"
rx={theme.borderRadius.soft}
ry={theme.borderRadius.soft}
{...this.defaultRectProps()}
/>}
/>
)}
<rect
className="bar"
fill={color}
@@ -137,13 +149,15 @@ class NodeResourcesMetricBox extends React.Component {
ry={theme.borderRadius.soft}
{...this.defaultRectProps(relativeConsumption)}
/>
{showInfo && <NodeResourcesMetricBoxInfo
{showInfo && (
<NodeResourcesMetricBoxInfo
label={label}
metricSummary={metricSummary}
width={width - (2 * RESOURCES_LABEL_PADDING)}
x={x + RESOURCES_LABEL_PADDING}
y={y + RESOURCES_LABEL_PADDING}
/>}
/>
)}
</g>
);
}
+4 -4
View File
@@ -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 (
<NodesError faIconClass="far fa-circle" hidden={!nodesDisplayEmpty}>
<div className="heading">Nothing to show. This can have any of these reasons:</div>
{topologyNodeCountZero ?
renderCauses(NODES_STATS_COUNT_ZERO_CAUSES) :
renderCauses(NODES_NOT_DISPLAYED_CAUSES)}
{topologyNodeCountZero
? renderCauses(NODES_STATS_COUNT_ZERO_CAUSES)
: renderCauses(NODES_NOT_DISPLAYED_CAUSES)}
</NodesError>
);
}
+10 -1
View File
@@ -10,7 +10,16 @@ const Plugin = ({
}) => {
const error = status !== 'ok';
const className = classNames({ error });
const tip = (<span>Description: {description}<br />Status: {status}</span>);
const tip = (
<span>
Description:
{description}
<br />
Status:
{' '}
{status}
</span>
);
// Inner span to hold styling so we don't effect the "before:content"
return (
+9 -2
View File
@@ -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 active={this.props.searchFocused && isEmpty(pinnedSearches)}>
{searchHint} <SearchHintIcon
{searchHint}
{' '}
<SearchHintIcon
className="fa fa-question-circle"
onMouseDown={this.props.toggleHelp}
/>
+3 -1
View File
@@ -24,7 +24,9 @@ export default class ShowMore extends React.PureComponent {
}
return (
<div className="show-more" onClick={this.handleClick}>
{limitActionText} <span className={`show-more-icon ${limitActionIcon}`} />
{limitActionText}
{' '}
<span className={`show-more-icon ${limitActionIcon}`} />
</div>
);
}
+6 -4
View File
@@ -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 && <circle
{hasData && (
<circle
className="sparkcircle"
cx={graph.lastX}
cy={graph.lastY}
@@ -130,7 +131,8 @@ export default class Sparkline extends React.Component {
fillOpacity={fillOpacity}
stroke="none"
r={radius}
/>}
/>
)}
</svg>
</div>
);
@@ -51,12 +51,14 @@ class TerminalApp extends React.Component {
return (
<div className="terminal-app" style={style}>
{this.props.controlPipe && <Terminal
{this.props.controlPipe && (
<Terminal
pipe={this.props.controlPipe}
titleBarColor={this.state.titleBarColor}
statusBarColor={this.state.statusBarColor}
title={this.state.title}
embedded={false} />}
embedded={false} />
)}
</div>
);
}
+6 -3
View File
@@ -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 {
</div>
);
}
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 };
}
@@ -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 {
</span>
</div>
</div>
{isPaused &&
{isPaused
&& (
<span className="time-control-info">
Showing state from <TimestampTag inheritStyles relative timestamp={pausedAt} />
Showing state from
{' '}
<TimestampTag inheritStyles relative timestamp={pausedAt} />
</span>
)
}
</div>
);
@@ -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) => {
+1 -1
View File
@@ -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) {
@@ -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 (
<div className="topology-option" key={optionId}>
<div className="topology-option-wrapper">
{option.get('selectType') === 'union' &&
{option.get('selectType') === 'union'
&& (
<TopologyOptionAction
onClick={this.handleNoneClick}
optionId={optionId}
@@ -109,6 +110,7 @@ class TopologyOptions extends React.Component {
topologyId={currentTopologyId}
activeValue={activeValue}
/>
)
}
{option.get('options').map(item => (
<TopologyOptionAction
@@ -36,11 +36,19 @@ class DebugMenu extends React.Component {
>
<i className="fa fa-code" />
<span className="description">Save raw data as JSON</span>
{pausedAt && <span className="soft"> ({pausedAt})</span>}
{pausedAt && (
<span className="soft">
{' '}
(
{pausedAt}
)
</span>
)}
</a>
</div>
<div className="troubleshooting-menu-item">
<button
type="button"
className="footer-icon"
onClick={this.props.clickDownloadGraph}
title="Save canvas as SVG (does not include search highlighting)"
@@ -52,6 +60,7 @@ class DebugMenu extends React.Component {
</div>
<div className="troubleshooting-menu-item">
<button
type="button"
className="footer-icon"
title="Reset view state"
onClick={this.handleClickReset}
@@ -3,7 +3,8 @@ import { connect } from 'react-redux';
import ViewModeButton from './view-mode-button';
import MetricSelector from './metric-selector';
import { setGraphView, setTableView, setResourceView } from '../actions/app-actions';
import { setResourceView } from '../actions/request-actions';
import { setGraphView, setTableView } from '../actions/app-actions';
import { availableMetricsSelector } from '../selectors/node-metric';
import {
isResourceViewModeSelector,
+3 -2
View File
@@ -12,8 +12,9 @@ class Warning extends React.Component {
}
handleClick() {
const expanded = !this.state.expanded;
this.setState({ expanded });
this.setState(prevState => ({
expanded: !prevState.expanded
}));
}
render() {
@@ -56,11 +56,11 @@ export default class ZoomControl extends React.Component {
return (
<div className="zoom-control">
<button className="zoom-in" onClick={this.handleZoomIn}>
<button className="zoom-in" type="button" onClick={this.handleZoomIn}>
<i className="fa fa-plus" />
</button>
<Slider value={value} max={1} step={SLIDER_STEP} vertical onChange={this.handleChange} />
<button className="zoom-out" onClick={this.handleZoomOut}>
<button className="zoom-out" type="button" onClick={this.handleZoomOut}>
<i className="fa fa-minus" />
</button>
</div>
@@ -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)}
</g>
</svg>
{this.canChangeZoom() && <ZoomControl
{this.canChangeZoom() && (
<ZoomControl
zoomAction={this.handleZoomControlAction}
minScale={this.state.minScale}
maxScale={this.state.maxScale}
scale={this.state.scaleX}
/>}
/>
)}
</div>
);
}
+3 -3
View File
@@ -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 }));
+3 -1
View File
@@ -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,
+64
View File
@@ -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;
};
}
+2 -3
View File
@@ -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(
@@ -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);
+3 -1
View File
@@ -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(
@@ -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', () => {
+2 -2
View File
@@ -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) {
+13
View File
@@ -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();
}
};
+3 -1
View File
@@ -1,4 +1,6 @@
import { isPlainObject, mapValues, isEmpty, omitBy } from 'lodash';
import {
isPlainObject, mapValues, isEmpty, omitBy
} from 'lodash';
export function hashDifferenceDeep(A, B) {
+3 -3
View File
@@ -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';
+5 -62
View File
@@ -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 = '<SLASH>';
const PERCENT = '%';
const PERCENT_REPLACEMENT = '<PERCENT>';
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;
};
}
+3 -3
View File
@@ -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) {
+9 -259
View File
@@ -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;
}
}
+9 -9
View File
@@ -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"
}
}
+174 -170
View File
@@ -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"