mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-15 19:41:02 +00:00
Migrate from Flux to Redux
* better state visibility * pure state changes * state debug panel (show: crtl-h, move: ctrl-w)
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
},
|
||||
"rules": {
|
||||
"comma-dangle": 0,
|
||||
"no-param-reassign": 0,
|
||||
"object-curly-spacing": 0,
|
||||
"react/jsx-closing-bracket-location": 0,
|
||||
"react/prefer-stateless-function": 0,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import debug from 'debug';
|
||||
|
||||
import AppDispatcher from '../dispatcher/app-dispatcher';
|
||||
import ActionTypes from '../constants/action-types';
|
||||
import { saveGraph } from '../utils/file-utils';
|
||||
import { modulo } from '../utils/math-utils';
|
||||
@@ -9,108 +8,122 @@ import { bufferDeltaUpdate, resumeUpdate,
|
||||
resetUpdateBuffer } from '../utils/update-buffer-utils';
|
||||
import { doControlRequest, getNodesDelta, getNodeDetails,
|
||||
getTopologies, deletePipe } from '../utils/web-api-utils';
|
||||
import AppStore from '../stores/app-store';
|
||||
import { getActiveTopologyOptions,
|
||||
getCurrentTopologyUrl } from '../utils/topology-utils';
|
||||
|
||||
const log = debug('scope:app-actions');
|
||||
|
||||
export function showHelp() {
|
||||
AppDispatcher.dispatch({type: ActionTypes.SHOW_HELP});
|
||||
return {type: ActionTypes.SHOW_HELP};
|
||||
}
|
||||
|
||||
export function hideHelp() {
|
||||
AppDispatcher.dispatch({type: ActionTypes.HIDE_HELP});
|
||||
return {type: ActionTypes.HIDE_HELP};
|
||||
}
|
||||
|
||||
export function toggleHelp() {
|
||||
if (AppStore.getShowingHelp()) {
|
||||
hideHelp();
|
||||
} else {
|
||||
showHelp();
|
||||
}
|
||||
return (dispatch, getState) => {
|
||||
if (getState().get('showingHelp')) {
|
||||
dispatch(hideHelp());
|
||||
}
|
||||
dispatch(showHelp());
|
||||
};
|
||||
}
|
||||
|
||||
export function selectMetric(metricId) {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
type: ActionTypes.SELECT_METRIC,
|
||||
metricId
|
||||
});
|
||||
}
|
||||
|
||||
export function pinNextMetric(delta) {
|
||||
const metrics = AppStore.getAvailableCanvasMetrics().map(m => m.get('id'));
|
||||
const currentIndex = metrics.indexOf(AppStore.getSelectedMetric());
|
||||
const nextIndex = modulo(currentIndex + delta, metrics.count());
|
||||
const nextMetric = metrics.get(nextIndex);
|
||||
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.PIN_METRIC,
|
||||
metricId: nextMetric,
|
||||
});
|
||||
updateRoute();
|
||||
};
|
||||
}
|
||||
|
||||
export function pinMetric(metricId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.PIN_METRIC,
|
||||
metricId,
|
||||
});
|
||||
updateRoute();
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.PIN_METRIC,
|
||||
metricId,
|
||||
});
|
||||
updateRoute(getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function unpinMetric() {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.UNPIN_METRIC,
|
||||
});
|
||||
updateRoute();
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.UNPIN_METRIC,
|
||||
});
|
||||
updateRoute(getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function pinNextMetric(delta) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const metrics = state.get('availableCanvasMetrics').map(m => m.get('id'));
|
||||
const currentIndex = metrics.indexOf(state.get('selectedMetric'));
|
||||
const nextIndex = modulo(currentIndex + delta, metrics.count());
|
||||
const nextMetric = metrics.get(nextIndex);
|
||||
|
||||
dispatch(pinMetric(nextMetric));
|
||||
};
|
||||
}
|
||||
|
||||
export function changeTopologyOption(option, value, topologyId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
|
||||
topologyId,
|
||||
option,
|
||||
value
|
||||
});
|
||||
updateRoute();
|
||||
// update all request workers with new options
|
||||
resetUpdateBuffer();
|
||||
getTopologies(
|
||||
AppStore.getActiveTopologyOptions()
|
||||
);
|
||||
getNodesDelta(
|
||||
AppStore.getCurrentTopologyUrl(),
|
||||
AppStore.getActiveTopologyOptions()
|
||||
);
|
||||
getNodeDetails(
|
||||
AppStore.getTopologyUrlsById(),
|
||||
AppStore.getNodeDetails()
|
||||
);
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
|
||||
topologyId,
|
||||
option,
|
||||
value
|
||||
});
|
||||
updateRoute(getState);
|
||||
// update all request workers with new options
|
||||
resetUpdateBuffer();
|
||||
const state = getState();
|
||||
getTopologies(getActiveTopologyOptions(state), dispatch);
|
||||
getNodesDelta(
|
||||
getCurrentTopologyUrl(state),
|
||||
getActiveTopologyOptions(state),
|
||||
dispatch
|
||||
);
|
||||
getNodeDetails(
|
||||
state.get('topologyUrlsById'),
|
||||
state.get('nodeDetails'),
|
||||
dispatch
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function clickBackground() {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_BACKGROUND
|
||||
});
|
||||
updateRoute();
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.CLICK_BACKGROUND
|
||||
});
|
||||
updateRoute(getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function clickCloseDetails(nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_CLOSE_DETAILS,
|
||||
nodeId
|
||||
});
|
||||
updateRoute();
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.CLICK_CLOSE_DETAILS,
|
||||
nodeId
|
||||
});
|
||||
updateRoute(getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function clickCloseTerminal(pipeId, closePipe) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_CLOSE_TERMINAL,
|
||||
pipeId
|
||||
});
|
||||
if (closePipe) {
|
||||
deletePipe(pipeId);
|
||||
}
|
||||
updateRoute();
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.CLICK_CLOSE_TERMINAL,
|
||||
pipeId
|
||||
});
|
||||
if (closePipe) {
|
||||
deletePipe(pipeId, dispatch);
|
||||
}
|
||||
updateRoute(getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function clickDownloadGraph() {
|
||||
@@ -118,285 +131,340 @@ export function clickDownloadGraph() {
|
||||
}
|
||||
|
||||
export function clickForceRelayout() {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_FORCE_RELAYOUT
|
||||
});
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: ActionTypes.CLICK_FORCE_RELAYOUT,
|
||||
forceRelayout: true
|
||||
});
|
||||
// fire only once, reset after dispatch
|
||||
setTimeout(() => {
|
||||
dispatch({
|
||||
type: ActionTypes.CLICK_FORCE_RELAYOUT,
|
||||
forceRelayout: false
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
}
|
||||
|
||||
export function clickNode(nodeId, label, origin) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_NODE,
|
||||
origin,
|
||||
label,
|
||||
nodeId
|
||||
});
|
||||
updateRoute();
|
||||
getNodeDetails(
|
||||
AppStore.getTopologyUrlsById(),
|
||||
AppStore.getNodeDetails()
|
||||
);
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.CLICK_NODE,
|
||||
origin,
|
||||
label,
|
||||
nodeId
|
||||
});
|
||||
updateRoute(getState);
|
||||
const state = getState();
|
||||
getNodeDetails(
|
||||
state.get('topologyUrlsById'),
|
||||
state.get('nodeDetails'),
|
||||
dispatch
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function clickPauseUpdate() {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
type: ActionTypes.CLICK_PAUSE_UPDATE
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function clickRelative(nodeId, topologyId, label, origin) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_RELATIVE,
|
||||
label,
|
||||
origin,
|
||||
nodeId,
|
||||
topologyId
|
||||
});
|
||||
updateRoute();
|
||||
getNodeDetails(
|
||||
AppStore.getTopologyUrlsById(),
|
||||
AppStore.getNodeDetails()
|
||||
);
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.CLICK_RELATIVE,
|
||||
label,
|
||||
origin,
|
||||
nodeId,
|
||||
topologyId
|
||||
});
|
||||
updateRoute(getState);
|
||||
const state = getState();
|
||||
getNodeDetails(
|
||||
state.get('topologyUrlsById'),
|
||||
state.get('nodeDetails'),
|
||||
dispatch
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function clickResumeUpdate() {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_RESUME_UPDATE
|
||||
});
|
||||
resumeUpdate();
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.CLICK_RESUME_UPDATE
|
||||
});
|
||||
resumeUpdate(getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function clickShowTopologyForNode(topologyId, nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE,
|
||||
topologyId,
|
||||
nodeId
|
||||
});
|
||||
updateRoute();
|
||||
resetUpdateBuffer();
|
||||
getNodesDelta(
|
||||
AppStore.getCurrentTopologyUrl(),
|
||||
AppStore.getActiveTopologyOptions()
|
||||
);
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE,
|
||||
topologyId,
|
||||
nodeId
|
||||
});
|
||||
updateRoute(getState);
|
||||
// update all request workers with new options
|
||||
resetUpdateBuffer();
|
||||
const state = getState();
|
||||
getNodesDelta(
|
||||
getCurrentTopologyUrl(state),
|
||||
getActiveTopologyOptions(state),
|
||||
dispatch
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function clickTopology(topologyId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_TOPOLOGY,
|
||||
topologyId
|
||||
});
|
||||
updateRoute();
|
||||
resetUpdateBuffer();
|
||||
getNodesDelta(
|
||||
AppStore.getCurrentTopologyUrl(),
|
||||
AppStore.getActiveTopologyOptions()
|
||||
);
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.CLICK_TOPOLOGY,
|
||||
topologyId
|
||||
});
|
||||
updateRoute(getState);
|
||||
// update all request workers with new options
|
||||
resetUpdateBuffer();
|
||||
const state = getState();
|
||||
getNodesDelta(
|
||||
getCurrentTopologyUrl(state),
|
||||
getActiveTopologyOptions(state),
|
||||
dispatch
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function openWebsocket() {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
type: ActionTypes.OPEN_WEBSOCKET
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function clearControlError(nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
type: ActionTypes.CLEAR_CONTROL_ERROR,
|
||||
nodeId
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function closeWebsocket() {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
type: ActionTypes.CLOSE_WEBSOCKET
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doControl(nodeId, control) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.DO_CONTROL,
|
||||
nodeId
|
||||
});
|
||||
doControlRequest(nodeId, control);
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: ActionTypes.DO_CONTROL,
|
||||
nodeId
|
||||
});
|
||||
doControlRequest(nodeId, control, dispatch);
|
||||
};
|
||||
}
|
||||
|
||||
export function enterEdge(edgeId) {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
type: ActionTypes.ENTER_EDGE,
|
||||
edgeId
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function enterNode(nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
type: ActionTypes.ENTER_NODE,
|
||||
nodeId
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function hitEsc() {
|
||||
const controlPipe = AppStore.getControlPipe();
|
||||
if (AppStore.getShowingHelp()) {
|
||||
hideHelp();
|
||||
} else if (controlPipe && controlPipe.get('status') === 'PIPE_DELETED') {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_CLOSE_TERMINAL,
|
||||
pipeId: controlPipe.get('id')
|
||||
});
|
||||
updateRoute();
|
||||
// Don't deselect node on ESC if there is a controlPipe (keep terminal open)
|
||||
} else if (AppStore.getTopCardNodeId() && !controlPipe) {
|
||||
AppDispatcher.dispatch({ type: ActionTypes.DESELECT_NODE });
|
||||
updateRoute();
|
||||
}
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const controlPipe = state.get('controlPipes').last();
|
||||
if (state.get('showingHelp')) {
|
||||
dispatch(hideHelp());
|
||||
} else if (controlPipe && controlPipe.get('status') === 'PIPE_DELETED') {
|
||||
dispatch({
|
||||
type: ActionTypes.CLICK_CLOSE_TERMINAL,
|
||||
pipeId: controlPipe.get('id')
|
||||
});
|
||||
updateRoute(getState);
|
||||
// Don't deselect node on ESC if there is a controlPipe (keep terminal open)
|
||||
} else if (state.get('nodeDetails').last() && !controlPipe) {
|
||||
dispatch({ type: ActionTypes.DESELECT_NODE });
|
||||
updateRoute(getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function leaveEdge(edgeId) {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
type: ActionTypes.LEAVE_EDGE,
|
||||
edgeId
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function leaveNode(nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
type: ActionTypes.LEAVE_NODE,
|
||||
nodeId
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveControlError(nodeId, err) {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
type: ActionTypes.DO_CONTROL_ERROR,
|
||||
nodeId,
|
||||
error: err
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveControlSuccess(nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
type: ActionTypes.DO_CONTROL_SUCCESS,
|
||||
nodeId
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveNodeDetails(details) {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
type: ActionTypes.RECEIVE_NODE_DETAILS,
|
||||
details
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveNodesDelta(delta) {
|
||||
if (AppStore.isUpdatePaused()) {
|
||||
bufferDeltaUpdate(delta);
|
||||
} else {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.RECEIVE_NODES_DELTA,
|
||||
delta
|
||||
});
|
||||
}
|
||||
return (dispatch, getState) => {
|
||||
if (delta.add || delta.update || delta.remove) {
|
||||
const state = getState();
|
||||
if (state.get('updatePausedAt') !== null) {
|
||||
bufferDeltaUpdate(delta);
|
||||
} else {
|
||||
dispatch({
|
||||
type: ActionTypes.RECEIVE_NODES_DELTA,
|
||||
delta
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function receiveTopologies(topologies) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.RECEIVE_TOPOLOGIES,
|
||||
topologies
|
||||
});
|
||||
getNodesDelta(
|
||||
AppStore.getCurrentTopologyUrl(),
|
||||
AppStore.getActiveTopologyOptions()
|
||||
);
|
||||
getNodeDetails(
|
||||
AppStore.getTopologyUrlsById(),
|
||||
AppStore.getNodeDetails()
|
||||
);
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.RECEIVE_TOPOLOGIES,
|
||||
topologies
|
||||
});
|
||||
const state = getState();
|
||||
getNodesDelta(
|
||||
getCurrentTopologyUrl(state),
|
||||
getActiveTopologyOptions(state),
|
||||
dispatch
|
||||
);
|
||||
getNodeDetails(
|
||||
state.get('topologyUrlsById'),
|
||||
state.get('nodeDetails'),
|
||||
dispatch
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveApiDetails(apiDetails) {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
type: ActionTypes.RECEIVE_API_DETAILS,
|
||||
hostname: apiDetails.hostname,
|
||||
version: apiDetails.version,
|
||||
plugins: apiDetails.plugins
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveControlNodeRemoved(nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.RECEIVE_CONTROL_NODE_REMOVED,
|
||||
nodeId
|
||||
});
|
||||
updateRoute();
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.RECEIVE_CONTROL_NODE_REMOVED,
|
||||
nodeId
|
||||
});
|
||||
updateRoute(getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveControlPipeFromParams(pipeId, rawTty) {
|
||||
// TODO add nodeId
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
type: ActionTypes.RECEIVE_CONTROL_PIPE,
|
||||
pipeId,
|
||||
rawTty
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveControlPipe(pipeId, nodeId, rawTty) {
|
||||
if (nodeId !== AppStore.getTopCardNodeId()) {
|
||||
log('Node was deselected before we could set up control!');
|
||||
deletePipe(pipeId);
|
||||
return;
|
||||
}
|
||||
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 = AppStore.getControlPipe();
|
||||
if (controlPipe && controlPipe.get('id') !== pipeId) {
|
||||
deletePipe(controlPipe.get('id'));
|
||||
}
|
||||
const controlPipe = state.get('controlPipes').last();
|
||||
if (controlPipe && controlPipe.get('id') !== pipeId) {
|
||||
deletePipe(controlPipe.get('id'), dispatch);
|
||||
}
|
||||
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.RECEIVE_CONTROL_PIPE,
|
||||
nodeId,
|
||||
pipeId,
|
||||
rawTty
|
||||
});
|
||||
dispatch({
|
||||
type: ActionTypes.RECEIVE_CONTROL_PIPE,
|
||||
nodeId,
|
||||
pipeId,
|
||||
rawTty
|
||||
});
|
||||
|
||||
updateRoute();
|
||||
updateRoute(getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveControlPipeStatus(pipeId, status) {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
type: ActionTypes.RECEIVE_CONTROL_PIPE_STATUS,
|
||||
pipeId,
|
||||
status
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveError(errorUrl) {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
errorUrl,
|
||||
type: ActionTypes.RECEIVE_ERROR
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveNotFound(nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
return {
|
||||
nodeId,
|
||||
type: ActionTypes.RECEIVE_NOT_FOUND
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function route(state) {
|
||||
AppDispatcher.dispatch({
|
||||
state,
|
||||
type: ActionTypes.ROUTE_TOPOLOGY
|
||||
});
|
||||
getTopologies(
|
||||
AppStore.getActiveTopologyOptions()
|
||||
);
|
||||
getNodesDelta(
|
||||
AppStore.getCurrentTopologyUrl(),
|
||||
AppStore.getActiveTopologyOptions()
|
||||
);
|
||||
getNodeDetails(
|
||||
AppStore.getTopologyUrlsById(),
|
||||
AppStore.getNodeDetails()
|
||||
);
|
||||
export function route(urlState) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
state: urlState,
|
||||
type: ActionTypes.ROUTE_TOPOLOGY
|
||||
});
|
||||
// update all request workers with new options
|
||||
const state = getState();
|
||||
getTopologies(getActiveTopologyOptions(state), dispatch);
|
||||
getNodesDelta(
|
||||
getCurrentTopologyUrl(state),
|
||||
getActiveTopologyOptions(state),
|
||||
dispatch
|
||||
);
|
||||
getNodeDetails(
|
||||
state.get('topologyUrlsById'),
|
||||
state.get('nodeDetails'),
|
||||
dispatch
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { enterEdge, leaveEdge } from '../actions/app-actions';
|
||||
|
||||
export default class Edge extends React.Component {
|
||||
class Edge extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
@@ -27,12 +26,15 @@ export default class Edge extends React.Component {
|
||||
}
|
||||
|
||||
handleMouseEnter(ev) {
|
||||
enterEdge(ev.currentTarget.id);
|
||||
this.props.enterEdge(ev.currentTarget.id);
|
||||
}
|
||||
|
||||
handleMouseLeave(ev) {
|
||||
leaveEdge(ev.currentTarget.id);
|
||||
this.props.leaveEdge(ev.currentTarget.id);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(Edge, PureRenderMixin);
|
||||
export default connect(
|
||||
null,
|
||||
{ enterEdge, leaveEdge }
|
||||
)(Edge);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { clickNode, enterNode, leaveNode } from '../actions/app-actions';
|
||||
@@ -45,7 +44,7 @@ function ellipsis(text, fontSize, maxWidth) {
|
||||
return truncatedText;
|
||||
}
|
||||
|
||||
export default class Node extends React.Component {
|
||||
class Node extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
@@ -116,18 +115,22 @@ export default class Node extends React.Component {
|
||||
|
||||
handleMouseClick(ev) {
|
||||
ev.stopPropagation();
|
||||
clickNode(this.props.id, this.props.label, ReactDOM.findDOMNode(this).getBoundingClientRect());
|
||||
this.props.clickNode(this.props.id, this.props.label,
|
||||
ReactDOM.findDOMNode(this).getBoundingClientRect());
|
||||
}
|
||||
|
||||
handleMouseEnter() {
|
||||
enterNode(this.props.id);
|
||||
this.props.enterNode(this.props.id);
|
||||
this.setState({ hovered: true });
|
||||
}
|
||||
|
||||
handleMouseLeave() {
|
||||
leaveNode(this.props.id);
|
||||
this.props.leaveNode(this.props.id);
|
||||
this.setState({ hovered: false });
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(Node, PureRenderMixin);
|
||||
export default connect(
|
||||
null,
|
||||
{ clickNode, enterNode, leaveNode }
|
||||
)(Node);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { hasSelectedNode as hasSelectedNodeFn } from '../utils/topology-utils';
|
||||
import EdgeContainer from './edge-container';
|
||||
|
||||
export default class NodesChartEdges extends React.Component {
|
||||
class NodesChartEdges extends React.Component {
|
||||
render() {
|
||||
const {hasSelectedNode, highlightedEdgeIds, layoutEdges, layoutPrecision,
|
||||
selectedNodeId} = this.props;
|
||||
@@ -36,4 +36,14 @@ export default class NodesChartEdges extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(NodesChartEdges, PureRenderMixin);
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
hasSelectedNode: hasSelectedNodeFn(state),
|
||||
selectedNodeId: state.get('selectedNodeId'),
|
||||
highlightedEdgeIds: state.get('highlightedEdgeIds')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(NodesChartEdges);
|
||||
|
||||
@@ -10,19 +10,11 @@ export default class NodesChartElements extends React.Component {
|
||||
const props = this.props;
|
||||
return (
|
||||
<g className="nodes-chart-elements" transform={props.transform}>
|
||||
<NodesChartEdges layoutEdges={props.edges} selectedNodeId={props.selectedNodeId}
|
||||
highlightedEdgeIds={props.highlightedEdgeIds}
|
||||
hasSelectedNode={props.hasSelectedNode}
|
||||
<NodesChartEdges layoutEdges={props.layoutEdges}
|
||||
layoutPrecision={props.layoutPrecision} />
|
||||
<NodesChartNodes layoutNodes={props.nodes} selectedNodeId={props.selectedNodeId}
|
||||
selectedMetric={props.selectedMetric}
|
||||
topCardNode={props.topCardNode}
|
||||
highlightedNodeIds={props.highlightedNodeIds}
|
||||
hasSelectedNode={props.hasSelectedNode}
|
||||
adjacentNodes={props.adjacentNodes}
|
||||
nodeScale={props.nodeScale} onNodeClick={props.onNodeClick}
|
||||
<NodesChartNodes layoutNodes={props.layoutNodes} nodeScale={props.nodeScale}
|
||||
scale={props.scale} selectedNodeScale={props.selectedNodeScale}
|
||||
topologyId={props.topologyId} layoutPrecision={props.layoutPrecision} />
|
||||
layoutPrecision={props.layoutPrecision} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { connect } from 'react-redux';
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
import { getAdjacentNodes } from '../utils/topology-utils';
|
||||
import NodeContainer from './node-container';
|
||||
|
||||
export default class NodesChartNodes extends React.Component {
|
||||
class NodesChartNodes extends React.Component {
|
||||
render() {
|
||||
const {adjacentNodes, highlightedNodeIds,
|
||||
layoutNodes, layoutPrecision, nodeScale, onNodeClick, scale,
|
||||
selectedMetric, selectedNodeScale, selectedNodeId, topologyId, topCardNode} = this.props;
|
||||
const { adjacentNodes, highlightedNodeIds, layoutNodes, layoutPrecision,
|
||||
nodeScale, scale, selectedMetric, selectedNodeScale, selectedNodeId,
|
||||
topologyId, topCardNode } = this.props;
|
||||
|
||||
const zoomScale = scale;
|
||||
|
||||
@@ -56,7 +56,6 @@ export default class NodesChartNodes extends React.Component {
|
||||
topologyId={topologyId}
|
||||
shape={node.get('shape')}
|
||||
stack={node.get('stack')}
|
||||
onClick={onNodeClick}
|
||||
key={node.get('id')}
|
||||
id={node.get('id')}
|
||||
label={node.get('label')}
|
||||
@@ -76,4 +75,17 @@ export default class NodesChartNodes extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(NodesChartNodes, PureRenderMixin);
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
adjacentNodes: getAdjacentNodes(state),
|
||||
highlightedNodeIds: state.get('highlightedNodeIds'),
|
||||
selectedMetric: state.get('selectedMetric'),
|
||||
selectedNodeId: state.get('selectedNodeId'),
|
||||
topologyId: state.get('topologyId'),
|
||||
topCardNode: state.get('nodeDetails').last()
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(NodesChartNodes);
|
||||
|
||||
@@ -2,8 +2,7 @@ import _ from 'lodash';
|
||||
import d3 from 'd3';
|
||||
import debug from 'debug';
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { connect } from 'react-redux';
|
||||
import { Map as makeMap, fromJS, is as isDeepEqual } from 'immutable';
|
||||
import timely from 'timely';
|
||||
|
||||
@@ -13,6 +12,8 @@ import { DETAILS_PANEL_WIDTH } from '../constants/styles';
|
||||
import Logo from '../components/logo';
|
||||
import { doLayout } from './nodes-layout';
|
||||
import NodesChartElements from './nodes-chart-elements';
|
||||
import { getActiveTopologyOptions, getAdjacentNodes,
|
||||
isSameTopology } from '../utils/topology-utils';
|
||||
|
||||
const log = debug('scope:nodes-chart');
|
||||
|
||||
@@ -29,7 +30,7 @@ const ZOOM_CACHE_FIELDS = ['scale', 'panTranslateX', 'panTranslateY'];
|
||||
const radiusDensity = d3.scale.threshold()
|
||||
.domain([3, 6]).range([2.5, 3.5, 3]);
|
||||
|
||||
export default class NodesChart extends React.Component {
|
||||
class NodesChart extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
@@ -88,7 +89,7 @@ export default class NodesChart extends React.Component {
|
||||
state.width = nextProps.forceRelayout ? nextProps.width : (state.width || nextProps.width);
|
||||
|
||||
// _.assign(state, this.updateGraphState(nextProps, state));
|
||||
if (nextProps.forceRelayout || nextProps.nodes !== this.props.nodes) {
|
||||
if (nextProps.forceRelayout || !isSameTopology(nextProps.nodes, this.props.nodes)) {
|
||||
_.assign(state, this.updateGraphState(nextProps, state));
|
||||
}
|
||||
|
||||
@@ -139,22 +140,10 @@ export default class NodesChart extends React.Component {
|
||||
<g transform="translate(24,24) scale(0.25)">
|
||||
<Logo />
|
||||
</g>
|
||||
<NodesChartElements
|
||||
edges={edges}
|
||||
nodes={nodes}
|
||||
transform={transform}
|
||||
adjacentNodes={this.props.adjacentNodes}
|
||||
layoutPrecision={this.props.layoutPrecision}
|
||||
selectedMetric={this.props.selectedMetric}
|
||||
selectedNodeId={this.props.selectedNodeId}
|
||||
highlightedEdgeIds={this.props.highlightedEdgeIds}
|
||||
highlightedNodeIds={this.props.highlightedNodeIds}
|
||||
hasSelectedNode={this.props.hasSelectedNode}
|
||||
nodeScale={this.state.nodeScale}
|
||||
scale={this.state.scale}
|
||||
<NodesChartElements layoutNodes={nodes} layoutEdges={edges}
|
||||
nodeScale={this.state.nodeScale} scale={scale} transform={transform}
|
||||
selectedNodeScale={this.state.selectedNodeScale}
|
||||
topCardNode={this.props.topCardNode}
|
||||
topologyId={this.props.topologyId} />
|
||||
layoutPrecision={this.props.layoutPrecision} />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
@@ -162,7 +151,7 @@ export default class NodesChart extends React.Component {
|
||||
|
||||
handleMouseClick() {
|
||||
if (!this.isZooming) {
|
||||
clickBackground();
|
||||
this.props.clickBackground();
|
||||
} else {
|
||||
this.isZooming = false;
|
||||
}
|
||||
@@ -411,4 +400,18 @@ export default class NodesChart extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(NodesChart, PureRenderMixin);
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
adjacentNodes: getAdjacentNodes(state),
|
||||
forceRelayout: state.get('forceRelayout'),
|
||||
nodes: state.get('nodes'),
|
||||
selectedNodeId: state.get('selectedNodeId'),
|
||||
topologyId: state.get('topologyId'),
|
||||
topologyOptions: getActiveTopologyOptions(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ clickBackground }
|
||||
)(NodesChart);
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import Immutable from 'immutable';
|
||||
import TestUtils from 'react/lib/ReactTestUtils';
|
||||
|
||||
jest.dontMock('../../dispatcher/app-dispatcher');
|
||||
jest.dontMock('../node-details.js');
|
||||
jest.dontMock('../node-details/node-details-controls.js');
|
||||
jest.dontMock('../node-details/node-details-relatives.js');
|
||||
@@ -13,7 +12,7 @@ jest.dontMock('../../utils/color-utils');
|
||||
jest.dontMock('../../utils/title-utils');
|
||||
|
||||
// need ES5 require to keep automocking off
|
||||
const NodeDetails = require('../node-details.js').default;
|
||||
const NodeDetails = require('../node-details.js').NodeDetails;
|
||||
|
||||
describe('NodeDetails', () => {
|
||||
let nodes;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import debug from 'debug';
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Logo from './logo';
|
||||
import AppStore from '../stores/app-store';
|
||||
import Footer from './footer.js';
|
||||
import Sidebar from './sidebar.js';
|
||||
import HelpPanel from './help-panel';
|
||||
@@ -19,67 +17,32 @@ import Nodes from './nodes';
|
||||
import MetricSelector from './metric-selector';
|
||||
import EmbeddedTerminal from './embedded-terminal';
|
||||
import { getRouter } from '../utils/router-utils';
|
||||
import { showingDebugToolbar, toggleDebugToolbar,
|
||||
DebugToolbar } from './debug-toolbar.js';
|
||||
import DebugToolbar, { showingDebugToolbar,
|
||||
toggleDebugToolbar } from './debug-toolbar.js';
|
||||
import { getUrlState } from '../utils/router-utils';
|
||||
import { getActiveTopologyOptions } from '../utils/topology-utils';
|
||||
|
||||
const ESC_KEY_CODE = 27;
|
||||
const keyPressLog = debug('scope:app-key-press');
|
||||
|
||||
/* make sure these can all be shallow-checked for equality for PureRenderMixin */
|
||||
function getStateFromStores() {
|
||||
return {
|
||||
activeTopologyOptions: AppStore.getActiveTopologyOptions(),
|
||||
adjacentNodes: AppStore.getAdjacentNodes(AppStore.getSelectedNodeId()),
|
||||
controlStatus: AppStore.getControlStatus(),
|
||||
controlPipe: AppStore.getControlPipe(),
|
||||
currentTopology: AppStore.getCurrentTopology(),
|
||||
currentTopologyId: AppStore.getCurrentTopologyId(),
|
||||
currentTopologyOptions: AppStore.getCurrentTopologyOptions(),
|
||||
errorUrl: AppStore.getErrorUrl(),
|
||||
forceRelayout: AppStore.isForceRelayout(),
|
||||
highlightedEdgeIds: AppStore.getHighlightedEdgeIds(),
|
||||
highlightedNodeIds: AppStore.getHighlightedNodeIds(),
|
||||
hostname: AppStore.getHostname(),
|
||||
pinnedMetric: AppStore.getPinnedMetric(),
|
||||
availableCanvasMetrics: AppStore.getAvailableCanvasMetrics(),
|
||||
nodeDetails: AppStore.getNodeDetails(),
|
||||
nodes: AppStore.getNodes(),
|
||||
showingHelp: AppStore.getShowingHelp(),
|
||||
selectedNodeId: AppStore.getSelectedNodeId(),
|
||||
selectedMetric: AppStore.getSelectedMetric(),
|
||||
topologies: AppStore.getTopologies(),
|
||||
topologiesLoaded: AppStore.isTopologiesLoaded(),
|
||||
topologyEmpty: AppStore.isTopologyEmpty(),
|
||||
updatePaused: AppStore.isUpdatePaused(),
|
||||
updatePausedAt: AppStore.getUpdatePausedAt(),
|
||||
version: AppStore.getVersion(),
|
||||
versionUpdate: AppStore.getVersionUpdate(),
|
||||
plugins: AppStore.getPlugins(),
|
||||
websocketClosed: AppStore.isWebsocketClosed()
|
||||
};
|
||||
}
|
||||
|
||||
export default class App extends React.Component {
|
||||
class App extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onKeyPress = this.onKeyPress.bind(this);
|
||||
this.onKeyUp = this.onKeyUp.bind(this);
|
||||
this.state = getStateFromStores();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AppStore.addListener(this.onChange);
|
||||
window.addEventListener('keypress', this.onKeyPress);
|
||||
window.addEventListener('keyup', this.onKeyUp);
|
||||
|
||||
getRouter().start({hashbang: true});
|
||||
if (!AppStore.isRouteSet()) {
|
||||
getRouter(this.props.dispatch, this.props.urlState).start({hashbang: true});
|
||||
if (!this.props.routeSet) {
|
||||
// dont request topologies when already done via router
|
||||
getTopologies(AppStore.getActiveTopologyOptions());
|
||||
getTopologies(this.props.activeTopologyOptions, this.props.dispatch);
|
||||
}
|
||||
getApiDetails();
|
||||
getApiDetails(this.props.dispatch);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -87,18 +50,15 @@ export default class App extends React.Component {
|
||||
window.removeEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.setState(getStateFromStores());
|
||||
}
|
||||
|
||||
onKeyUp(ev) {
|
||||
// don't get esc in onKeyPress
|
||||
if (ev.keyCode === ESC_KEY_CODE) {
|
||||
hitEsc();
|
||||
this.props.dispatch(hitEsc());
|
||||
}
|
||||
}
|
||||
|
||||
onKeyPress(ev) {
|
||||
const { dispatch } = this.props;
|
||||
//
|
||||
// keyup gives 'key'
|
||||
// keypress gives 'char'
|
||||
@@ -108,42 +68,35 @@ export default class App extends React.Component {
|
||||
keyPressLog('onKeyPress', 'keyCode', ev.keyCode, ev);
|
||||
const char = String.fromCharCode(ev.charCode);
|
||||
if (char === '<') {
|
||||
pinNextMetric(-1);
|
||||
dispatch(pinNextMetric(-1));
|
||||
} else if (char === '>') {
|
||||
pinNextMetric(1);
|
||||
dispatch(pinNextMetric(1));
|
||||
} else if (char === 'q') {
|
||||
unpinMetric();
|
||||
selectMetric(null);
|
||||
dispatch(unpinMetric());
|
||||
dispatch(selectMetric(null));
|
||||
} else if (char === 'd') {
|
||||
toggleDebugToolbar();
|
||||
this.forceUpdate();
|
||||
} else if (char === '?') {
|
||||
toggleHelp();
|
||||
dispatch(toggleHelp());
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { nodeDetails, controlPipe } = this.state;
|
||||
const topCardNode = nodeDetails.last();
|
||||
const { availableCanvasMetrics, nodeDetails, controlPipes, showingHelp } = this.props;
|
||||
const showingDetails = nodeDetails.size > 0;
|
||||
const showingTerminal = controlPipe;
|
||||
// width of details panel blocking a view
|
||||
const detailsWidth = showingDetails ? 450 : 0;
|
||||
const topMargin = 100;
|
||||
const showingTerminal = controlPipes.size > 0;
|
||||
const showingMetricsSelector = availableCanvasMetrics.count() > 0;
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
{showingDebugToolbar() && <DebugToolbar />}
|
||||
|
||||
{this.state.showingHelp && <HelpPanel />}
|
||||
{showingHelp && <HelpPanel />}
|
||||
|
||||
{showingDetails && <Details nodes={this.state.nodes}
|
||||
controlStatus={this.state.controlStatus}
|
||||
details={this.state.nodeDetails} />}
|
||||
{showingDetails && <Details />}
|
||||
|
||||
{showingTerminal && <EmbeddedTerminal
|
||||
pipe={this.state.controlPipe}
|
||||
details={this.state.nodeDetails} />}
|
||||
{showingTerminal && <EmbeddedTerminal />}
|
||||
|
||||
<div className="header">
|
||||
<div className="logo">
|
||||
@@ -151,45 +104,35 @@ export default class App extends React.Component {
|
||||
<Logo />
|
||||
</svg>
|
||||
</div>
|
||||
<Topologies topologies={this.state.topologies}
|
||||
currentTopology={this.state.currentTopology} />
|
||||
<Topologies />
|
||||
</div>
|
||||
|
||||
<Nodes
|
||||
nodes={this.state.nodes}
|
||||
highlightedNodeIds={this.state.highlightedNodeIds}
|
||||
highlightedEdgeIds={this.state.highlightedEdgeIds}
|
||||
detailsWidth={detailsWidth}
|
||||
selectedNodeId={this.state.selectedNodeId}
|
||||
topMargin={topMargin}
|
||||
topCardNode={topCardNode}
|
||||
selectedMetric={this.state.selectedMetric}
|
||||
forceRelayout={this.state.forceRelayout}
|
||||
topologyOptions={this.state.activeTopologyOptions}
|
||||
topologyEmpty={this.state.topologyEmpty}
|
||||
adjacentNodes={this.state.adjacentNodes}
|
||||
topologyId={this.state.currentTopologyId} />
|
||||
<Nodes />
|
||||
|
||||
<Sidebar>
|
||||
<Status errorUrl={this.state.errorUrl} topology={this.state.currentTopology}
|
||||
topologiesLoaded={this.state.topologiesLoaded}
|
||||
websocketClosed={this.state.websocketClosed} />
|
||||
{this.state.availableCanvasMetrics.count() > 0 && <MetricSelector
|
||||
availableCanvasMetrics={this.state.availableCanvasMetrics}
|
||||
pinnedMetric={this.state.pinnedMetric}
|
||||
selectedMetric={this.state.selectedMetric}
|
||||
/>}
|
||||
<TopologyOptions options={this.state.currentTopologyOptions}
|
||||
topologyId={this.state.currentTopologyId}
|
||||
activeOptions={this.state.activeTopologyOptions} />
|
||||
<Status />
|
||||
{showingMetricsSelector && <MetricSelector />}
|
||||
<TopologyOptions />
|
||||
</Sidebar>
|
||||
|
||||
<Footer hostname={this.state.hostname} plugins={this.state.plugins}
|
||||
updatePaused={this.state.updatePaused} updatePausedAt={this.state.updatePausedAt}
|
||||
version={this.state.version} versionUpdate={this.state.versionUpdate} />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(App, PureRenderMixin);
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
activeTopologyOptions: getActiveTopologyOptions(state),
|
||||
availableCanvasMetrics: state.get('availableCanvasMetrics'),
|
||||
controlPipes: state.get('controlPipes'),
|
||||
nodeDetails: state.get('nodeDetails'),
|
||||
routeSet: state.get('routeSet'),
|
||||
showingHelp: state.get('showingHelp'),
|
||||
urlState: getUrlState(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(App);
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import Perf from 'react-addons-perf';
|
||||
import { connect } from 'react-redux';
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
import debug from 'debug';
|
||||
const log = debug('scope:debug-panel');
|
||||
|
||||
import { receiveNodesDelta } from '../actions/app-actions';
|
||||
import AppStore from '../stores/app-store';
|
||||
import { getNodeColor, getNodeColorDark } from '../utils/color-utils';
|
||||
|
||||
|
||||
@@ -56,11 +57,10 @@ const deltaAdd = (name, adjacency = [], shape = 'circle', stack = false, nodeCou
|
||||
});
|
||||
|
||||
|
||||
function addMetrics(node, v) {
|
||||
const availableMetrics = AppStore.getAvailableCanvasMetrics().toJS();
|
||||
const metrics = availableMetrics.length > 0 ? availableMetrics : [
|
||||
function addMetrics(availableMetrics, node, v) {
|
||||
const metrics = availableMetrics.size > 0 ? availableMetrics : fromJS([
|
||||
{id: 'host_cpu_usage_percent', label: 'CPU'}
|
||||
];
|
||||
]);
|
||||
|
||||
return Object.assign({}, node, {
|
||||
metrics: metrics.map(m => Object.assign({}, m, {max: 100, value: v}))
|
||||
@@ -74,26 +74,26 @@ function label(shape, stacked) {
|
||||
}
|
||||
|
||||
|
||||
function addAllVariants() {
|
||||
function addAllVariants(dispatch) {
|
||||
const newNodes = _.flattenDeep(STACK_VARIANTS.map(stack => (SHAPES.map(s => {
|
||||
if (!stack) return [deltaAdd(label(s, stack), [], s, stack, 1)];
|
||||
return NODE_COUNTS.map(n => deltaAdd(label(s, stack), [], s, stack, n));
|
||||
}))));
|
||||
|
||||
receiveNodesDelta({
|
||||
dispatch(receiveNodesDelta({
|
||||
add: newNodes
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
function addAllMetricVariants() {
|
||||
function addAllMetricVariants(availableMetrics, dispatch) {
|
||||
const newNodes = _.flattenDeep(METRIC_FILLS.map((v, i) => (
|
||||
SHAPES.map(s => [addMetrics(deltaAdd(label(s) + i, [], s), v)])
|
||||
SHAPES.map(s => [addMetrics(availableMetrics, deltaAdd(label(s) + i, [], s), v)])
|
||||
)));
|
||||
|
||||
receiveNodesDelta({
|
||||
dispatch(receiveNodesDelta({
|
||||
add: newNodes
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -109,27 +109,6 @@ function startPerf(delay) {
|
||||
setTimeout(stopPerf, delay * 1000);
|
||||
}
|
||||
|
||||
|
||||
function addNodes(n, prefix = 'zing') {
|
||||
const ns = AppStore.getNodes();
|
||||
const nodeNames = ns.keySeq().toJS();
|
||||
const newNodeNames = _.range(ns.size, ns.size + n).map(i => (
|
||||
// `${randomLetter()}${randomLetter()}-zing`
|
||||
`${prefix}${i}`
|
||||
));
|
||||
const allNodes = _(nodeNames).concat(newNodeNames).value();
|
||||
|
||||
receiveNodesDelta({
|
||||
add: newNodeNames.map((name) => deltaAdd(
|
||||
name,
|
||||
sample(allNodes),
|
||||
_.sample(SHAPES),
|
||||
_.sample(STACK_VARIANTS),
|
||||
_.sample(NODE_COUNTS)
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
export function showingDebugToolbar() {
|
||||
return (('debugToolbar' in localStorage && JSON.parse(localStorage.debugToolbar))
|
||||
|| location.pathname.indexOf('debug') > -1);
|
||||
@@ -153,12 +132,13 @@ function disableLog() {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
export class DebugToolbar extends React.Component {
|
||||
class DebugToolbar extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.toggleColors = this.toggleColors.bind(this);
|
||||
this.addNodes = this.addNodes.bind(this);
|
||||
this.state = {
|
||||
nodesToAdd: 30,
|
||||
showColors: false
|
||||
@@ -175,20 +155,44 @@ export class DebugToolbar extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
addNodes(n, prefix = 'zing') {
|
||||
const ns = this.props.nodes;
|
||||
const nodeNames = ns.keySeq().toJS();
|
||||
const newNodeNames = _.range(ns.size, ns.size + n).map(i => (
|
||||
// `${randomLetter()}${randomLetter()}-zing`
|
||||
`${prefix}${i}`
|
||||
));
|
||||
const allNodes = _(nodeNames).concat(newNodeNames).value();
|
||||
|
||||
this.props.dispatch(receiveNodesDelta({
|
||||
add: newNodeNames.map((name) => deltaAdd(
|
||||
name,
|
||||
sample(allNodes),
|
||||
_.sample(SHAPES),
|
||||
_.sample(STACK_VARIANTS),
|
||||
_.sample(NODE_COUNTS)
|
||||
))
|
||||
}));
|
||||
|
||||
log('added nodes', n);
|
||||
}
|
||||
|
||||
render() {
|
||||
log('rending debug panel');
|
||||
const { availableCanvasMetrics } = this.props;
|
||||
|
||||
return (
|
||||
<div className="debug-panel">
|
||||
<div>
|
||||
<label>Add nodes </label>
|
||||
<button onClick={() => addNodes(1)}>+1</button>
|
||||
<button onClick={() => addNodes(10)}>+10</button>
|
||||
<button onClick={() => this.addNodes(1)}>+1</button>
|
||||
<button onClick={() => this.addNodes(10)}>+10</button>
|
||||
<input type="number" onChange={this.onChange} value={this.state.nodesToAdd} />
|
||||
<button onClick={() => addNodes(this.state.nodesToAdd)}>+</button>
|
||||
<button onClick={() => addAllVariants()}>Variants</button>
|
||||
<button onClick={() => addAllMetricVariants()}>Metric Variants</button>
|
||||
<button onClick={() => addNodes(1, LOREM)}>Long name</button>
|
||||
<button onClick={() => this.addNodes(this.state.nodesToAdd)}>+</button>
|
||||
<button onClick={() => addAllVariants(this.props.dispatch)}>Variants</button>
|
||||
<button onClick={() => addAllMetricVariants(availableCanvasMetrics, this.props.dispatch)}>
|
||||
Metric Variants
|
||||
</button>
|
||||
<button onClick={() => this.addNodes(1, LOREM)}>Long name</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -228,3 +232,14 @@ export class DebugToolbar extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
nodes: state.get('nodes'),
|
||||
availableCanvasMetrics: state.get('availableCanvasMetrics')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(DebugToolbar);
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import DetailsCard from './details-card';
|
||||
|
||||
export default function Details({controlStatus, details, nodes}) {
|
||||
// render all details as cards, later cards go on top
|
||||
return (
|
||||
<div className="details">
|
||||
{details.toIndexedSeq().map((obj, index) => <DetailsCard key={obj.id}
|
||||
index={index} cardCount={details.size} nodes={nodes}
|
||||
nodeControlStatus={controlStatus.get(obj.id)} {...obj} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
class Details extends React.Component {
|
||||
render() {
|
||||
const { controlStatus, details } = this.props;
|
||||
// render all details as cards, later cards go on top
|
||||
return (
|
||||
<div className="details">
|
||||
{details.toIndexedSeq().map((obj, index) => <DetailsCard key={obj.id}
|
||||
index={index} cardCount={details.size}
|
||||
nodeControlStatus={controlStatus.get(obj.id)} {...obj} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
controlStatus: state.get('controlStatus'),
|
||||
details: state.get('nodeDetails')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(Details);
|
||||
|
||||
11
client/app/scripts/components/dev-tools.js
Normal file
11
client/app/scripts/components/dev-tools.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { createDevTools } from 'redux-devtools';
|
||||
import LogMonitor from 'redux-devtools-log-monitor';
|
||||
import DockMonitor from 'redux-devtools-dock-monitor';
|
||||
|
||||
export default createDevTools(
|
||||
<DockMonitor defaultIsVisible={false}
|
||||
toggleVisibilityKey="ctrl-h" changePositionKey="ctrl-w">
|
||||
<LogMonitor />
|
||||
</DockMonitor>
|
||||
);
|
||||
@@ -1,29 +1,45 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getNodeColor, getNodeColorDark } from '../utils/color-utils';
|
||||
import Terminal from './terminal';
|
||||
import { DETAILS_PANEL_WIDTH, DETAILS_PANEL_MARGINS,
|
||||
DETAILS_PANEL_OFFSET } from '../constants/styles';
|
||||
|
||||
export default function EmeddedTerminal({pipe, details}) {
|
||||
const nodeId = pipe.get('nodeId');
|
||||
const node = details.get(nodeId);
|
||||
const d = node && node.details;
|
||||
const titleBarColor = d && getNodeColorDark(d.rank, d.label);
|
||||
const statusBarColor = d && getNodeColor(d.rank, d.label);
|
||||
const title = d && d.label;
|
||||
class EmeddedTerminal extends React.Component {
|
||||
render() {
|
||||
const { pipe, details } = this.props;
|
||||
const nodeId = pipe.get('nodeId');
|
||||
const node = details.get(nodeId);
|
||||
const d = node && node.details;
|
||||
const titleBarColor = d && getNodeColorDark(d.rank, d.label);
|
||||
const statusBarColor = d && getNodeColor(d.rank, d.label);
|
||||
const title = d && d.label;
|
||||
|
||||
const style = {
|
||||
right: DETAILS_PANEL_MARGINS.right + DETAILS_PANEL_WIDTH + 10 +
|
||||
(details.size * DETAILS_PANEL_OFFSET)
|
||||
};
|
||||
const style = {
|
||||
right: DETAILS_PANEL_MARGINS.right + DETAILS_PANEL_WIDTH + 10 +
|
||||
(details.size * DETAILS_PANEL_OFFSET)
|
||||
};
|
||||
|
||||
// React unmount/remounts when key changes, this is important for cleaning up
|
||||
// the term.js and creating a new one for the new pipe.
|
||||
return (
|
||||
<div className="terminal-embedded" style={style}>
|
||||
<Terminal key={pipe.get('id')} pipe={pipe} titleBarColor={titleBarColor}
|
||||
statusBarColor={statusBarColor} containerMargin={style.right} title={title} />
|
||||
</div>
|
||||
);
|
||||
// React unmount/remounts when key changes, this is important for cleaning up
|
||||
// the term.js and creating a new one for the new pipe.
|
||||
return (
|
||||
<div className="terminal-embedded" style={style}>
|
||||
<Terminal key={pipe.get('id')} pipe={pipe} titleBarColor={titleBarColor}
|
||||
statusBarColor={statusBarColor} containerMargin={style.right}
|
||||
title={title} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
details: state.get('nodeDetails'),
|
||||
pipe: state.get('controlPipes').last()
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(EmeddedTerminal);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import moment from 'moment';
|
||||
|
||||
import Plugins from './plugins.js';
|
||||
@@ -8,83 +9,103 @@ import { clickDownloadGraph, clickForceRelayout, clickPauseUpdate,
|
||||
clickResumeUpdate, toggleHelp } from '../actions/app-actions';
|
||||
import { basePathSlash } from '../utils/web-api-utils';
|
||||
|
||||
export default function Footer(props) {
|
||||
const { hostname, plugins, updatePaused, updatePausedAt, version, versionUpdate } = props;
|
||||
const contrastMode = isContrastMode();
|
||||
class Footer extends React.Component {
|
||||
render() {
|
||||
const { hostname, updatePausedAt, version, versionUpdate } = this.props;
|
||||
const contrastMode = isContrastMode();
|
||||
|
||||
// link url to switch contrast with current UI state
|
||||
const otherContrastModeUrl = contrastMode
|
||||
? basePathSlash(window.location.pathname) : contrastModeUrl;
|
||||
const otherContrastModeTitle = contrastMode
|
||||
? 'Switch to normal contrast' : 'Switch to high contrast';
|
||||
const forceRelayoutTitle = 'Force re-layout (might reduce edge crossings, '
|
||||
+ 'but may shift nodes around)';
|
||||
// link url to switch contrast with current UI state
|
||||
const otherContrastModeUrl = contrastMode
|
||||
? basePathSlash(window.location.pathname) : contrastModeUrl;
|
||||
const otherContrastModeTitle = contrastMode
|
||||
? 'Switch to normal contrast' : 'Switch to high contrast';
|
||||
const forceRelayoutTitle = 'Force re-layout (might reduce edge crossings, '
|
||||
+ 'but may shift nodes around)';
|
||||
|
||||
// pause button
|
||||
const isPaused = updatePaused;
|
||||
const updateCount = getUpdateBufferSize();
|
||||
const hasUpdates = updateCount > 0;
|
||||
const pausedAgo = moment(updatePausedAt).fromNow();
|
||||
const pauseTitle = isPaused
|
||||
? `Paused ${pausedAgo}` : 'Pause updates (freezes the nodes in their current layout)';
|
||||
const pauseAction = isPaused ? clickResumeUpdate : clickPauseUpdate;
|
||||
const pauseClassName = isPaused ? 'footer-icon footer-icon-active' : 'footer-icon';
|
||||
let pauseLabel = '';
|
||||
if (hasUpdates && isPaused) {
|
||||
pauseLabel = `Paused +${updateCount}`;
|
||||
} else if (hasUpdates && !isPaused) {
|
||||
pauseLabel = `Resuming +${updateCount}`;
|
||||
} else if (!hasUpdates && isPaused) {
|
||||
pauseLabel = 'Paused';
|
||||
// pause button
|
||||
const isPaused = updatePausedAt !== null;
|
||||
const updateCount = getUpdateBufferSize();
|
||||
const hasUpdates = updateCount > 0;
|
||||
const pausedAgo = moment(updatePausedAt).fromNow();
|
||||
const pauseTitle = isPaused
|
||||
? `Paused ${pausedAgo}` : 'Pause updates (freezes the nodes in their current layout)';
|
||||
const pauseAction = isPaused ? this.props.clickResumeUpdate : this.props.clickPauseUpdate;
|
||||
const pauseClassName = isPaused ? 'footer-icon footer-icon-active' : 'footer-icon';
|
||||
let pauseLabel = '';
|
||||
if (hasUpdates && isPaused) {
|
||||
pauseLabel = `Paused +${updateCount}`;
|
||||
} else if (hasUpdates && !isPaused) {
|
||||
pauseLabel = `Resuming +${updateCount}`;
|
||||
} else if (!hasUpdates && isPaused) {
|
||||
pauseLabel = 'Paused';
|
||||
}
|
||||
|
||||
const versionUpdateTitle = versionUpdate
|
||||
? `New version available: ${versionUpdate.version}. Click to download`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div className="footer">
|
||||
|
||||
<div className="footer-status">
|
||||
{versionUpdate && <a className="footer-versionupdate"
|
||||
title={versionUpdateTitle} href={versionUpdate.downloadUrl} target="_blank">
|
||||
Update available: {versionUpdate.version}
|
||||
</a>}
|
||||
<span className="footer-label">Version</span>
|
||||
{version}
|
||||
<span className="footer-label">on</span>
|
||||
{hostname}
|
||||
</div>
|
||||
|
||||
<div className="footer-plugins">
|
||||
<Plugins />
|
||||
</div>
|
||||
|
||||
<div className="footer-tools">
|
||||
<a className={pauseClassName} onClick={pauseAction} title={pauseTitle}>
|
||||
{pauseLabel !== '' && <span className="footer-label">{pauseLabel}</span>}
|
||||
<span className="fa fa-pause" />
|
||||
</a>
|
||||
<a className="footer-icon" onClick={this.props.clickForceRelayout}
|
||||
title={forceRelayoutTitle}>
|
||||
<span className="fa fa-refresh" />
|
||||
</a>
|
||||
<a className="footer-icon" onClick={this.props.clickDownloadGraph}
|
||||
title="Save canvas as SVG">
|
||||
<span className="fa fa-download" />
|
||||
</a>
|
||||
<a className="footer-icon" href="api/report" download title="Save raw data as JSON">
|
||||
<span className="fa fa-code" />
|
||||
</a>
|
||||
<a className="footer-icon" href={otherContrastModeUrl} title={otherContrastModeTitle}>
|
||||
<span className="fa fa-adjust" />
|
||||
</a>
|
||||
<a className="footer-icon" href="https://gitreports.com/issue/weaveworks/scope" target="_blank" title="Report an issue">
|
||||
<span className="fa fa-bug" />
|
||||
</a>
|
||||
<a className="footer-icon" onClick={this.props.toggleHelp}
|
||||
title="Show help">
|
||||
<span className="fa fa-question" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const versionUpdateTitle = versionUpdate
|
||||
? `New version available: ${versionUpdate.version}. Click to download`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div className="footer">
|
||||
|
||||
<div className="footer-status">
|
||||
{versionUpdate && <a className="footer-versionupdate"
|
||||
title={versionUpdateTitle} href={versionUpdate.downloadUrl} target="_blank">
|
||||
Update available: {versionUpdate.version}
|
||||
</a>}
|
||||
<span className="footer-label">Version</span>
|
||||
{version}
|
||||
<span className="footer-label">on</span>
|
||||
{hostname}
|
||||
</div>
|
||||
|
||||
<div className="footer-plugins">
|
||||
<Plugins plugins={plugins} />
|
||||
</div>
|
||||
|
||||
<div className="footer-tools">
|
||||
<a className={pauseClassName} onClick={pauseAction} title={pauseTitle}>
|
||||
{pauseLabel !== '' && <span className="footer-label">{pauseLabel}</span>}
|
||||
<span className="fa fa-pause" />
|
||||
</a>
|
||||
<a className="footer-icon" onClick={clickForceRelayout} title={forceRelayoutTitle}>
|
||||
<span className="fa fa-refresh" />
|
||||
</a>
|
||||
<a className="footer-icon" onClick={clickDownloadGraph} title="Save canvas as SVG">
|
||||
<span className="fa fa-download" />
|
||||
</a>
|
||||
<a className="footer-icon" href="api/report" download title="Save raw data as JSON">
|
||||
<span className="fa fa-code" />
|
||||
</a>
|
||||
<a className="footer-icon" href={otherContrastModeUrl} title={otherContrastModeTitle}>
|
||||
<span className="fa fa-adjust" />
|
||||
</a>
|
||||
<a className="footer-icon" href="https://gitreports.com/issue/weaveworks/scope" target="_blank" title="Report an issue">
|
||||
<span className="fa fa-bug" />
|
||||
</a>
|
||||
<a className="footer-icon" onClick={toggleHelp} title="Show help">
|
||||
<span className="fa fa-question" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
hostname: state.get('hostname'),
|
||||
updatePausedAt: state.get('updatePausedAt'),
|
||||
version: state.get('version'),
|
||||
versionUpdate: state.get('versionUpdate')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ clickDownloadGraph, clickForceRelayout, clickPauseUpdate,
|
||||
clickResumeUpdate, toggleHelp }
|
||||
)(Footer);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { selectMetric, pinMetric, unpinMetric } from '../actions/app-actions';
|
||||
|
||||
|
||||
export class MetricSelectorItem extends React.Component {
|
||||
class MetricSelectorItem extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
@@ -14,7 +15,7 @@ export class MetricSelectorItem extends React.Component {
|
||||
|
||||
onMouseOver() {
|
||||
const k = this.props.metric.get('id');
|
||||
selectMetric(k);
|
||||
this.props.selectMetric(k);
|
||||
}
|
||||
|
||||
onMouseClick() {
|
||||
@@ -22,9 +23,9 @@ export class MetricSelectorItem extends React.Component {
|
||||
const pinnedMetric = this.props.pinnedMetric;
|
||||
|
||||
if (k === pinnedMetric) {
|
||||
unpinMetric(k);
|
||||
this.props.unpinMetric(k);
|
||||
} else {
|
||||
pinMetric(k);
|
||||
this.props.pinMetric(k);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,3 +50,15 @@ export class MetricSelectorItem extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
selectedMetric: state.get('selectedMetric'),
|
||||
pinnedMetric: state.get('pinnedMetric')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ selectMetric, pinMetric, unpinMetric }
|
||||
)(MetricSelectorItem);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { selectMetric } from '../actions/app-actions';
|
||||
import { MetricSelectorItem } from './metric-selector-item';
|
||||
import MetricSelectorItem from './metric-selector-item';
|
||||
|
||||
|
||||
export default class MetricSelector extends React.Component {
|
||||
class MetricSelector extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
@@ -11,19 +12,18 @@ export default class MetricSelector extends React.Component {
|
||||
}
|
||||
|
||||
onMouseOut() {
|
||||
selectMetric(this.props.pinnedMetric);
|
||||
this.props.selectMetric(this.props.pinnedMetric);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {availableCanvasMetrics} = this.props;
|
||||
|
||||
const items = availableCanvasMetrics.map(metric => (
|
||||
<MetricSelectorItem key={metric.get('id')} metric={metric} {...this.props} />
|
||||
<MetricSelectorItem key={metric.get('id')} metric={metric} />
|
||||
));
|
||||
|
||||
return (
|
||||
<div
|
||||
className="metric-selector">
|
||||
<div className="metric-selector">
|
||||
<div className="metric-selector-wrapper" onMouseLeave={this.onMouseOut}>
|
||||
{items}
|
||||
</div>
|
||||
@@ -32,3 +32,14 @@ export default class MetricSelector extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
availableCanvasMetrics: state.get('availableCanvasMetrics'),
|
||||
pinnedMetric: state.get('pinnedMetric')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ selectMetric }
|
||||
)(MetricSelector);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import NodeDetailsControls from './node-details/node-details-controls';
|
||||
import NodeDetailsHealth from './node-details/node-details-health';
|
||||
@@ -11,7 +12,7 @@ import { clickCloseDetails, clickShowTopologyForNode } from '../actions/app-acti
|
||||
import { brightenColor, getNeutralColor, getNodeColorDark } from '../utils/color-utils';
|
||||
import { resetDocumentTitle, setDocumentTitle } from '../utils/title-utils';
|
||||
|
||||
export default class NodeDetails extends React.Component {
|
||||
export class NodeDetails extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
@@ -21,12 +22,12 @@ export default class NodeDetails extends React.Component {
|
||||
|
||||
handleClickClose(ev) {
|
||||
ev.preventDefault();
|
||||
clickCloseDetails(this.props.nodeId);
|
||||
this.props.clickCloseDetails(this.props.nodeId);
|
||||
}
|
||||
|
||||
handleShowTopologyForNode(ev) {
|
||||
ev.preventDefault();
|
||||
clickShowTopologyForNode(this.props.topologyId, this.props.nodeId);
|
||||
this.props.clickShowTopologyForNode(this.props.topologyId, this.props.nodeId);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -215,3 +216,14 @@ export default class NodeDetails extends React.Component {
|
||||
setDocumentTitle(this.props.details && this.props.details.label);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
nodes: state.get('nodes')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ clickCloseDetails, clickShowTopologyForNode }
|
||||
)(NodeDetails);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { doControl } from '../../actions/app-actions';
|
||||
|
||||
export default class NodeDetailsControlButton extends React.Component {
|
||||
class NodeDetailsControlButton extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
@@ -20,6 +21,8 @@ export default class NodeDetailsControlButton extends React.Component {
|
||||
|
||||
handleClick(ev) {
|
||||
ev.preventDefault();
|
||||
doControl(this.props.nodeId, this.props.control);
|
||||
this.props.dispatch(doControl(this.props.nodeId, this.props.control));
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(NodeDetailsControlButton);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { clickRelative } from '../../actions/app-actions';
|
||||
|
||||
export default class NodeDetailsRelativesLink extends React.Component {
|
||||
class NodeDetailsRelativesLink extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
@@ -14,8 +13,8 @@ export default class NodeDetailsRelativesLink extends React.Component {
|
||||
|
||||
handleClick(ev) {
|
||||
ev.preventDefault();
|
||||
clickRelative(this.props.id, this.props.topologyId, this.props.label,
|
||||
ReactDOM.findDOMNode(this).getBoundingClientRect());
|
||||
this.props.dispatch(clickRelative(this.props.id, this.props.topologyId,
|
||||
this.props.label, ReactDOM.findDOMNode(this).getBoundingClientRect()));
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -28,4 +27,4 @@ export default class NodeDetailsRelativesLink extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(NodeDetailsRelativesLink, PureRenderMixin);
|
||||
export default connect()(NodeDetailsRelativesLink);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { clickRelative } from '../../actions/app-actions';
|
||||
|
||||
export default class NodeDetailsTableNodeLink extends React.Component {
|
||||
class NodeDetailsTableNodeLink extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
@@ -14,8 +13,8 @@ export default class NodeDetailsTableNodeLink extends React.Component {
|
||||
|
||||
handleClick(ev) {
|
||||
ev.preventDefault();
|
||||
clickRelative(this.props.nodeId, this.props.topologyId, this.props.label,
|
||||
ReactDOM.findDOMNode(this).getBoundingClientRect());
|
||||
this.props.dispatch(clickRelative(this.props.nodeId, this.props.topologyId,
|
||||
this.props.label, ReactDOM.findDOMNode(this).getBoundingClientRect()));
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -35,4 +34,4 @@ export default class NodeDetailsTableNodeLink extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(NodeDetailsTableNodeLink, PureRenderMixin);
|
||||
export default connect()(NodeDetailsTableNodeLink);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import NodesChart from '../charts/nodes-chart';
|
||||
import NodesError from '../charts/nodes-error';
|
||||
import { isTopologyEmpty } from '../utils/topology-utils';
|
||||
|
||||
const navbarHeight = 160;
|
||||
const marginTop = 0;
|
||||
const detailsWidth = 450;
|
||||
|
||||
/**
|
||||
* dynamic coords precision based on topology size
|
||||
@@ -26,7 +27,7 @@ function getLayoutPrecision(nodesCount) {
|
||||
return precision;
|
||||
}
|
||||
|
||||
export default class Nodes extends React.Component {
|
||||
class Nodes extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.handleResize = this.handleResize.bind(this);
|
||||
@@ -70,7 +71,8 @@ export default class Nodes extends React.Component {
|
||||
return (
|
||||
<div className="nodes-wrapper">
|
||||
{topologyEmpty && errorEmpty}
|
||||
<NodesChart {...this.props} {...this.state}
|
||||
<NodesChart {...this.state}
|
||||
detailsWidth={detailsWidth}
|
||||
layoutPrecision={layoutPrecision}
|
||||
hasSelectedNode={hasSelectedNode}
|
||||
/>
|
||||
@@ -90,4 +92,14 @@ export default class Nodes extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(Nodes, PureRenderMixin);
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
nodes: state.get('nodes'),
|
||||
selectedNodeId: state.get('selectedNodeId'),
|
||||
topologyEmpty: isTopologyEmpty(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(Nodes);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class Plugins extends React.Component {
|
||||
class Plugins extends React.Component {
|
||||
renderPlugin({id, label, description, status}) {
|
||||
const error = status !== 'ok';
|
||||
const className = classNames({ error });
|
||||
@@ -19,15 +20,26 @@ export default class Plugins extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const hasPlugins = this.props.plugins && this.props.plugins.length > 0;
|
||||
const hasPlugins = this.props.plugins && this.props.plugins.size > 0;
|
||||
return (
|
||||
<div className="plugins">
|
||||
<span className="plugins-label">
|
||||
Plugins:
|
||||
</span>
|
||||
{hasPlugins && this.props.plugins.map((plugin, index) => this.renderPlugin(plugin, index))}
|
||||
{hasPlugins && this.props.plugins.toIndexedSeq()
|
||||
.map((plugin, index) => this.renderPlugin(plugin, index))}
|
||||
{!hasPlugins && <span className="plugins-empty">n/a</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
plugins: state.get('plugins')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(Plugins);
|
||||
|
||||
@@ -1,36 +1,54 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
export default function Status({errorUrl, topologiesLoaded, topology, websocketClosed}) {
|
||||
let title = '';
|
||||
let text = 'Trying to reconnect...';
|
||||
let showWarningIcon = false;
|
||||
let classNames = 'status sidebar-item';
|
||||
class Status extends React.Component {
|
||||
render() {
|
||||
const {errorUrl, topologiesLoaded, topology, websocketClosed} = this.props;
|
||||
|
||||
if (errorUrl) {
|
||||
title = `Cannot reach Scope. Make sure the following URL is reachable: ${errorUrl}`;
|
||||
classNames += ' status-loading';
|
||||
showWarningIcon = true;
|
||||
} else if (!topologiesLoaded) {
|
||||
text = 'Connecting to Scope...';
|
||||
classNames += ' status-loading';
|
||||
showWarningIcon = true;
|
||||
} else if (websocketClosed) {
|
||||
classNames += ' status-loading';
|
||||
showWarningIcon = true;
|
||||
} else if (topology) {
|
||||
const stats = topology.get('stats');
|
||||
text = `${stats.get('node_count')} nodes`;
|
||||
if (stats.get('filtered_nodes')) {
|
||||
text = `${text} (${stats.get('filtered_nodes')} filtered)`;
|
||||
let title = '';
|
||||
let text = 'Trying to reconnect...';
|
||||
let showWarningIcon = false;
|
||||
let classNames = 'status sidebar-item';
|
||||
|
||||
if (errorUrl) {
|
||||
title = `Cannot reach Scope. Make sure the following URL is reachable: ${errorUrl}`;
|
||||
classNames += ' status-loading';
|
||||
showWarningIcon = true;
|
||||
} else if (!topologiesLoaded) {
|
||||
text = 'Connecting to Scope...';
|
||||
classNames += ' status-loading';
|
||||
showWarningIcon = true;
|
||||
} else if (websocketClosed) {
|
||||
classNames += ' status-loading';
|
||||
showWarningIcon = true;
|
||||
} else if (topology) {
|
||||
const stats = topology.get('stats');
|
||||
text = `${stats.get('node_count')} nodes`;
|
||||
if (stats.get('filtered_nodes')) {
|
||||
text = `${text} (${stats.get('filtered_nodes')} filtered)`;
|
||||
}
|
||||
classNames += ' status-stats';
|
||||
showWarningIcon = false;
|
||||
}
|
||||
classNames += ' status-stats';
|
||||
showWarningIcon = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
{showWarningIcon && <span className="status-icon fa fa-exclamation-circle" />}
|
||||
<span className="status-label" title={title}>{text}</span>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={classNames}>
|
||||
{showWarningIcon && <span className="status-icon fa fa-exclamation-circle" />}
|
||||
<span className="status-label" title={title}>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
errorUrl: state.get('errorUrl'),
|
||||
topologiesLoaded: state.get('topologiesLoaded'),
|
||||
topology: state.get('currentTopology'),
|
||||
websocketClosed: state.get('websocketClosed')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(Status);
|
||||
|
||||
@@ -1,48 +1,32 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import AppStore from '../stores/app-store';
|
||||
import Terminal from './terminal';
|
||||
import { receiveControlPipeFromParams } from '../actions/app-actions';
|
||||
|
||||
function getStateFromStores() {
|
||||
return {
|
||||
controlPipe: AppStore.getControlPipe()
|
||||
};
|
||||
}
|
||||
|
||||
export class TerminalApp extends React.Component {
|
||||
class TerminalApp extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
|
||||
const paramString = window.location.hash.split('/').pop();
|
||||
const params = JSON.parse(decodeURIComponent(paramString));
|
||||
receiveControlPipeFromParams(params.pipe.id, null, params.pipe.raw, false);
|
||||
this.props.receiveControlPipeFromParams(params.pipe.id, null, params.pipe.raw, false);
|
||||
|
||||
this.state = {
|
||||
title: params.title,
|
||||
titleBarColor: params.titleBarColor,
|
||||
statusBarColor: params.statusBarColor,
|
||||
controlPipe: AppStore.getControlPipe()
|
||||
statusBarColor: params.statusBarColor
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AppStore.addListener(this.onChange);
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.setState(getStateFromStores());
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = {borderTop: `4px solid ${this.state.titleBarColor}`};
|
||||
|
||||
return (
|
||||
<div className="terminal-app" style={style}>
|
||||
{this.state.controlPipe && <Terminal
|
||||
pipe={this.state.controlPipe}
|
||||
{this.props.controlPipe && <Terminal
|
||||
pipe={this.props.controlPipe}
|
||||
titleBarColor={this.state.titleBarColor}
|
||||
statusBarColor={this.state.statusBarColor}
|
||||
title={this.state.title}
|
||||
@@ -51,3 +35,14 @@ export class TerminalApp extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
controlPipe: state.get('controlPipes').last()
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ receiveControlPipeFromParams }
|
||||
)(TerminalApp);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import debug from 'debug';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { clickCloseTerminal } from '../actions/app-actions';
|
||||
@@ -73,7 +74,7 @@ function openNewWindow(url, bcr, minWidth = 200) {
|
||||
window.open(url, '', windowOptionsString);
|
||||
}
|
||||
|
||||
export default class Terminal extends React.Component {
|
||||
class Terminal extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
@@ -96,7 +97,7 @@ export default class Terminal extends React.Component {
|
||||
const socket = new WebSocket(`${wsUrl}/api/pipe/${this.getPipeId()}`);
|
||||
socket.binaryType = 'arraybuffer';
|
||||
|
||||
getPipeStatus(this.getPipeId());
|
||||
getPipeStatus(this.getPipeId(), this.props.dispatch);
|
||||
|
||||
socket.onopen = () => {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
@@ -210,7 +211,7 @@ export default class Terminal extends React.Component {
|
||||
handleCloseClick(ev) {
|
||||
ev.preventDefault();
|
||||
if (this.isEmbedded()) {
|
||||
clickCloseTerminal(this.getPipeId(), true);
|
||||
this.props.dispatch(clickCloseTerminal(this.getPipeId(), true));
|
||||
} else {
|
||||
window.close();
|
||||
}
|
||||
@@ -219,7 +220,7 @@ export default class Terminal extends React.Component {
|
||||
handlePopoutTerminal(ev) {
|
||||
ev.preventDefault();
|
||||
const paramString = JSON.stringify(this.props);
|
||||
clickCloseTerminal(this.getPipeId());
|
||||
this.props.dispatch(clickCloseTerminal(this.getPipeId()));
|
||||
|
||||
const bcr = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||
const minWidth = this.state.pixelPerCol * 80 + (8 * 2);
|
||||
@@ -322,3 +323,5 @@ export default class Terminal extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(Terminal);
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { clickTopology } from '../actions/app-actions';
|
||||
|
||||
export default class Topologies extends React.Component {
|
||||
class Topologies extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onTopologyClick = this.onTopologyClick.bind(this);
|
||||
this.renderSubTopology = this.renderSubTopology.bind(this);
|
||||
}
|
||||
|
||||
onTopologyClick(ev) {
|
||||
ev.preventDefault();
|
||||
clickTopology(ev.currentTarget.getAttribute('rel'));
|
||||
this.props.clickTopology(ev.currentTarget.getAttribute('rel'));
|
||||
}
|
||||
|
||||
renderSubTopology(subTopology) {
|
||||
@@ -55,7 +53,7 @@ export default class Topologies extends React.Component {
|
||||
</div>
|
||||
<div className="topologies-sub">
|
||||
{topology.has('sub_topologies')
|
||||
&& topology.get('sub_topologies').map(this.renderSubTopology)}
|
||||
&& topology.get('sub_topologies').map(subTop => this.renderSubTopology(subTop))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -72,4 +70,14 @@ export default class Topologies extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(Topologies, PureRenderMixin);
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
topologies: state.get('topologies'),
|
||||
currentTopology: state.get('currentTopology')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ clickTopology }
|
||||
)(Topologies);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { changeTopologyOption } from '../actions/app-actions';
|
||||
|
||||
export default class TopologyOptionAction extends React.Component {
|
||||
class TopologyOptionAction extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
@@ -14,7 +13,7 @@ export default class TopologyOptionAction extends React.Component {
|
||||
onClick(ev) {
|
||||
ev.preventDefault();
|
||||
const { optionId, topologyId, item } = this.props;
|
||||
changeTopologyOption(optionId, item.get('value'), topologyId);
|
||||
this.props.changeTopologyOption(optionId, item.get('value'), topologyId);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -29,4 +28,7 @@ export default class TopologyOptionAction extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(TopologyOptionAction, PureRenderMixin);
|
||||
export default connect(
|
||||
null,
|
||||
{ changeTopologyOption }
|
||||
)(TopologyOptionAction);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import reactMixin from 'react-mixin';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getActiveTopologyOptions, getCurrentTopologyOptions } from '../utils/topology-utils';
|
||||
import TopologyOptionAction from './topology-option-action';
|
||||
|
||||
export default class TopologyOptions extends React.Component {
|
||||
class TopologyOptions extends React.Component {
|
||||
|
||||
renderOption(option) {
|
||||
const { activeOptions, topologyId } = this.props;
|
||||
@@ -26,10 +26,21 @@ export default class TopologyOptions extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="topology-options">
|
||||
{this.props.options.toIndexedSeq().map(option => this.renderOption(option))}
|
||||
{this.props.options && this.props.options.toIndexedSeq().map(
|
||||
option => this.renderOption(option))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
reactMixin.onClass(TopologyOptions, PureRenderMixin);
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
options: getCurrentTopologyOptions(state),
|
||||
topologyId: state.get('currentTopologyId'),
|
||||
activeOptions: getActiveTopologyOptions(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(TopologyOptions);
|
||||
|
||||
@@ -2,9 +2,19 @@ require('font-awesome-webpack');
|
||||
require('../styles/contrast.less');
|
||||
require('../images/favicon.ico');
|
||||
|
||||
import 'babel-polyfill';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import configureStore from './stores/configureStore';
|
||||
import App from './components/app.js';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('app'));
|
||||
const store = configureStore();
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import Immutable from 'immutable';
|
||||
import installDevTools from 'immutable-devtools';
|
||||
installDevTools(Immutable);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Dispatcher } from 'flux';
|
||||
import _ from 'lodash';
|
||||
import debug from 'debug';
|
||||
const log = debug('scope:dispatcher');
|
||||
|
||||
const instance = new Dispatcher();
|
||||
|
||||
instance.dispatch = _.wrap(Dispatcher.prototype.dispatch, (func, payload) => {
|
||||
log(payload.type, payload);
|
||||
func.call(instance, payload);
|
||||
});
|
||||
|
||||
export default instance;
|
||||
export const dispatch = instance.dispatch.bind(instance);
|
||||
28
client/app/scripts/main.dev.js
Normal file
28
client/app/scripts/main.dev.js
Normal file
@@ -0,0 +1,28 @@
|
||||
require('font-awesome-webpack');
|
||||
require('../styles/main.less');
|
||||
require('../images/favicon.ico');
|
||||
|
||||
import 'babel-polyfill';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import configureStore from './stores/configureStore';
|
||||
import App from './components/app';
|
||||
|
||||
import DevTools from './components/dev-tools';
|
||||
import Immutable from 'immutable';
|
||||
import installDevTools from 'immutable-devtools';
|
||||
installDevTools(Immutable);
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<div>
|
||||
<App />
|
||||
<DevTools />
|
||||
</div>
|
||||
</Provider>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
@@ -1,10 +1,5 @@
|
||||
require('font-awesome-webpack');
|
||||
require('../styles/main.less');
|
||||
require('../images/favicon.ico');
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import App from './components/app.js';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('app'));
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./main.prod');
|
||||
} else {
|
||||
module.exports = require('./main.dev');
|
||||
}
|
||||
|
||||
22
client/app/scripts/main.prod.js
Normal file
22
client/app/scripts/main.prod.js
Normal file
@@ -0,0 +1,22 @@
|
||||
require('font-awesome-webpack');
|
||||
require('../styles/main.less');
|
||||
require('../images/favicon.ico');
|
||||
|
||||
import 'babel-polyfill';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import configureStore from './stores/configureStore';
|
||||
import App from './components/app';
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<div>
|
||||
<App />
|
||||
</div>
|
||||
</Provider>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
461
client/app/scripts/reducers/__tests__/root-test.js
Normal file
461
client/app/scripts/reducers/__tests__/root-test.js
Normal file
@@ -0,0 +1,461 @@
|
||||
jest.dontMock('../../utils/router-utils');
|
||||
jest.dontMock('../../utils/topology-utils');
|
||||
jest.dontMock('../../constants/action-types');
|
||||
jest.dontMock('../root');
|
||||
|
||||
const is = require('immutable').is;
|
||||
|
||||
// Root reducer test suite using Jasmine matchers
|
||||
|
||||
describe('RootReducer', () => {
|
||||
const ActionTypes = require('../../constants/action-types').default;
|
||||
const reducer = require('../root').default;
|
||||
const initialState = require('../root').initialState;
|
||||
const topologyUtils = require('../../utils/topology-utils');
|
||||
// TODO maybe extract those to topology-utils tests?
|
||||
const getActiveTopologyOptions = topologyUtils.getActiveTopologyOptions;
|
||||
const getAdjacentNodes = topologyUtils.getAdjacentNodes;
|
||||
const isTopologyEmpty = topologyUtils.isTopologyEmpty;
|
||||
const getUrlState = require('../../utils/router-utils').getUrlState;
|
||||
|
||||
// fixtures
|
||||
|
||||
const NODE_SET = {
|
||||
n1: {
|
||||
id: 'n1',
|
||||
rank: undefined,
|
||||
adjacency: ['n1', 'n2'],
|
||||
pseudo: undefined,
|
||||
label: undefined,
|
||||
label_minor: undefined
|
||||
},
|
||||
n2: {
|
||||
id: 'n2',
|
||||
rank: undefined,
|
||||
adjacency: undefined,
|
||||
pseudo: undefined,
|
||||
label: undefined,
|
||||
label_minor: undefined
|
||||
}
|
||||
};
|
||||
|
||||
// actions
|
||||
|
||||
const ChangeTopologyOptionAction = {
|
||||
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
|
||||
topologyId: 'topo1',
|
||||
option: 'option1',
|
||||
value: 'on'
|
||||
};
|
||||
|
||||
const ChangeTopologyOptionAction2 = {
|
||||
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
|
||||
topologyId: 'topo1',
|
||||
option: 'option1',
|
||||
value: 'off'
|
||||
};
|
||||
|
||||
const ClickNodeAction = {
|
||||
type: ActionTypes.CLICK_NODE,
|
||||
nodeId: 'n1'
|
||||
};
|
||||
|
||||
const ClickNode2Action = {
|
||||
type: ActionTypes.CLICK_NODE,
|
||||
nodeId: 'n2'
|
||||
};
|
||||
|
||||
const ClickRelativeAction = {
|
||||
type: ActionTypes.CLICK_RELATIVE,
|
||||
nodeId: 'rel1'
|
||||
};
|
||||
|
||||
const ClickShowTopologyForNodeAction = {
|
||||
type: ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE,
|
||||
topologyId: 'topo2',
|
||||
nodeId: 'rel1'
|
||||
};
|
||||
|
||||
const ClickSubTopologyAction = {
|
||||
type: ActionTypes.CLICK_TOPOLOGY,
|
||||
topologyId: 'topo1-grouped'
|
||||
};
|
||||
|
||||
const ClickTopologyAction = {
|
||||
type: ActionTypes.CLICK_TOPOLOGY,
|
||||
topologyId: 'topo1'
|
||||
};
|
||||
|
||||
const ClickTopology2Action = {
|
||||
type: ActionTypes.CLICK_TOPOLOGY,
|
||||
topologyId: 'topo2'
|
||||
};
|
||||
|
||||
const CloseWebsocketAction = {
|
||||
type: ActionTypes.CLOSE_WEBSOCKET
|
||||
};
|
||||
|
||||
const deSelectNode = {
|
||||
type: ActionTypes.DESELECT_NODE
|
||||
};
|
||||
|
||||
const OpenWebsocketAction = {
|
||||
type: ActionTypes.OPEN_WEBSOCKET
|
||||
};
|
||||
|
||||
const ReceiveNodesDeltaAction = {
|
||||
type: ActionTypes.RECEIVE_NODES_DELTA,
|
||||
delta: {
|
||||
add: [{
|
||||
id: 'n1',
|
||||
adjacency: ['n1', 'n2']
|
||||
}, {
|
||||
id: 'n2'
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const ReceiveNodesDeltaUpdateAction = {
|
||||
type: ActionTypes.RECEIVE_NODES_DELTA,
|
||||
delta: {
|
||||
update: [{
|
||||
id: 'n1',
|
||||
adjacency: ['n1']
|
||||
}],
|
||||
remove: ['n2']
|
||||
}
|
||||
};
|
||||
|
||||
const ReceiveTopologiesAction = {
|
||||
type: ActionTypes.RECEIVE_TOPOLOGIES,
|
||||
topologies: [{
|
||||
url: '/topo1',
|
||||
name: 'Topo1',
|
||||
options: [{
|
||||
id: 'option1',
|
||||
defaultValue: 'off',
|
||||
options: [
|
||||
{value: 'on'},
|
||||
{value: 'off'}
|
||||
]
|
||||
}],
|
||||
stats: {
|
||||
node_count: 1
|
||||
},
|
||||
sub_topologies: [{
|
||||
url: '/topo1-grouped',
|
||||
name: 'topo 1 grouped'
|
||||
}]
|
||||
}, {
|
||||
url: '/topo2',
|
||||
name: 'Topo2',
|
||||
stats: {
|
||||
node_count: 0
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
const RouteAction = {
|
||||
type: ActionTypes.ROUTE_TOPOLOGY,
|
||||
state: {}
|
||||
};
|
||||
|
||||
// Basic tests
|
||||
|
||||
it('returns initial state', () => {
|
||||
const nextState = reducer(undefined, {});
|
||||
expect(is(nextState, initialState)).toBeTruthy();
|
||||
});
|
||||
|
||||
// topology tests
|
||||
|
||||
it('init with no topologies', () => {
|
||||
const nextState = reducer(undefined, {});
|
||||
expect(nextState.get('topologies').size).toBe(0);
|
||||
expect(nextState.get('currentTopology')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('get current topology', () => {
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, ReceiveTopologiesAction);
|
||||
nextState = reducer(nextState, ClickTopologyAction);
|
||||
|
||||
expect(nextState.get('topologies').size).toBe(2);
|
||||
expect(nextState.get('currentTopology').get('name')).toBe('Topo1');
|
||||
expect(nextState.get('currentTopology').get('url')).toBe('/topo1');
|
||||
expect(nextState.get('currentTopology').get('options').first().get('id')).toBe('option1');
|
||||
});
|
||||
|
||||
it('get sub-topology', () => {
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, ReceiveTopologiesAction);
|
||||
nextState = reducer(nextState, ClickSubTopologyAction);
|
||||
|
||||
expect(nextState.get('topologies').size).toBe(2);
|
||||
expect(nextState.get('currentTopology').get('name')).toBe('topo 1 grouped');
|
||||
expect(nextState.get('currentTopology').get('url')).toBe('/topo1-grouped');
|
||||
expect(nextState.get('currentTopology').get('options')).toBeUndefined();
|
||||
});
|
||||
|
||||
// topology options
|
||||
|
||||
it('changes topology option', () => {
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, ReceiveTopologiesAction);
|
||||
nextState = reducer(nextState, ClickTopologyAction);
|
||||
|
||||
// default options
|
||||
expect(getActiveTopologyOptions(nextState).has('option1')).toBeTruthy();
|
||||
expect(getActiveTopologyOptions(nextState).get('option1')).toBe('off');
|
||||
expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('off');
|
||||
|
||||
// turn on
|
||||
nextState = reducer(nextState, ChangeTopologyOptionAction);
|
||||
expect(getActiveTopologyOptions(nextState).get('option1')).toBe('on');
|
||||
expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('on');
|
||||
|
||||
// turn off
|
||||
nextState = reducer(nextState, ChangeTopologyOptionAction2);
|
||||
expect(getActiveTopologyOptions(nextState).get('option1')).toBe('off');
|
||||
expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('off');
|
||||
|
||||
// sub-topology should retain main topo options
|
||||
nextState = reducer(nextState, ClickSubTopologyAction);
|
||||
expect(getActiveTopologyOptions(nextState).get('option1')).toBe('off');
|
||||
expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('off');
|
||||
|
||||
// other topology w/o options dont return options, but keep in app state
|
||||
nextState = reducer(nextState, ClickTopology2Action);
|
||||
expect(getActiveTopologyOptions(nextState)).toBeUndefined();
|
||||
expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('off');
|
||||
});
|
||||
|
||||
it('sets topology options from route', () => {
|
||||
RouteAction.state = {
|
||||
topologyId: 'topo1',
|
||||
selectedNodeId: null,
|
||||
topologyOptions: {topo1: {option1: 'on'}}};
|
||||
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, RouteAction);
|
||||
expect(getActiveTopologyOptions(nextState).get('option1')).toBe('on');
|
||||
expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('on');
|
||||
|
||||
// stay same after topos have been received
|
||||
nextState = reducer(nextState, ReceiveTopologiesAction);
|
||||
nextState = reducer(nextState, ClickTopologyAction);
|
||||
expect(getActiveTopologyOptions(nextState).get('option1')).toBe('on');
|
||||
expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('on');
|
||||
});
|
||||
|
||||
it('uses default topology options from route', () => {
|
||||
RouteAction.state = {
|
||||
topologyId: 'topo1',
|
||||
selectedNodeId: null,
|
||||
topologyOptions: null};
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, RouteAction);
|
||||
nextState = reducer(nextState, ReceiveTopologiesAction);
|
||||
nextState = reducer(nextState, ClickTopologyAction);
|
||||
expect(getActiveTopologyOptions(nextState).get('option1')).toBe('off');
|
||||
expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('off');
|
||||
});
|
||||
|
||||
// nodes delta
|
||||
|
||||
it('replaces adjacency on update', () => {
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, ReceiveNodesDeltaAction);
|
||||
expect(nextState.get('nodes').toJS().n1.adjacency).toEqual(['n1', 'n2']);
|
||||
nextState = reducer(nextState, ReceiveNodesDeltaUpdateAction);
|
||||
expect(nextState.get('nodes').toJS().n1.adjacency).toEqual(['n1']);
|
||||
});
|
||||
|
||||
// browsing
|
||||
|
||||
it('shows nodes that were received', () => {
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, ReceiveNodesDeltaAction);
|
||||
expect(nextState.get('nodes').toJS()).toEqual(NODE_SET);
|
||||
});
|
||||
|
||||
it('knows a route was set', () => {
|
||||
let nextState = initialState;
|
||||
expect(nextState.get('routeSet')).toBeFalsy();
|
||||
nextState = reducer(nextState, RouteAction);
|
||||
expect(nextState.get('routeSet')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('gets selected node after click', () => {
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, ReceiveNodesDeltaAction);
|
||||
nextState = reducer(nextState, ClickNodeAction);
|
||||
|
||||
expect(nextState.get('selectedNodeId')).toBe('n1');
|
||||
expect(nextState.get('nodes').toJS()).toEqual(NODE_SET);
|
||||
|
||||
nextState = reducer(nextState, deSelectNode);
|
||||
expect(nextState.get('selectedNodeId')).toBe(null);
|
||||
expect(nextState.get('nodes').toJS()).toEqual(NODE_SET);
|
||||
});
|
||||
|
||||
it('keeps showing nodes on navigating back after node click', () => {
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, ReceiveTopologiesAction);
|
||||
nextState = reducer(nextState, ClickTopologyAction);
|
||||
nextState = reducer(nextState, ReceiveNodesDeltaAction);
|
||||
expect(getUrlState(nextState).selectedNodeId).toEqual(null);
|
||||
|
||||
nextState = reducer(nextState, ClickNodeAction);
|
||||
expect(getUrlState(nextState).selectedNodeId).toEqual('n1');
|
||||
|
||||
// go back in browsing
|
||||
RouteAction.state = {topologyId: 'topo1', selectedNodeId: null};
|
||||
nextState = reducer(nextState, RouteAction);
|
||||
expect(nextState.get('selectedNodeId')).toBe(null);
|
||||
expect(nextState.get('nodes').toJS()).toEqual(NODE_SET);
|
||||
});
|
||||
|
||||
it('closes details when changing topologies', () => {
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, ReceiveTopologiesAction);
|
||||
nextState = reducer(nextState, ClickTopologyAction);
|
||||
nextState = reducer(nextState, ReceiveNodesDeltaAction);
|
||||
|
||||
expect(getUrlState(nextState).selectedNodeId).toEqual(null);
|
||||
expect(getUrlState(nextState).topologyId).toEqual('topo1');
|
||||
|
||||
nextState = reducer(nextState, ClickNodeAction);
|
||||
expect(getUrlState(nextState).selectedNodeId).toEqual('n1');
|
||||
expect(getUrlState(nextState).topologyId).toEqual('topo1');
|
||||
|
||||
nextState = reducer(nextState, ClickSubTopologyAction);
|
||||
expect(getUrlState(nextState).selectedNodeId).toEqual(null);
|
||||
expect(getUrlState(nextState).topologyId).toEqual('topo1-grouped');
|
||||
});
|
||||
|
||||
// connection errors
|
||||
|
||||
it('resets topology on websocket reconnect', () => {
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, ReceiveNodesDeltaAction);
|
||||
expect(nextState.get('nodes').toJS()).toEqual(NODE_SET);
|
||||
|
||||
nextState = reducer(nextState, CloseWebsocketAction);
|
||||
expect(nextState.get('websocketClosed')).toBeTruthy();
|
||||
// keep showing old nodes
|
||||
expect(nextState.get('nodes').toJS()).toEqual(NODE_SET);
|
||||
|
||||
nextState = reducer(nextState, OpenWebsocketAction);
|
||||
expect(nextState.get('websocketClosed')).toBeFalsy();
|
||||
// opened socket clears nodes
|
||||
expect(nextState.get('nodes').toJS()).toEqual({});
|
||||
});
|
||||
|
||||
// adjacency test
|
||||
|
||||
it('returns the correct adjacency set for a node', () => {
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, ReceiveNodesDeltaAction);
|
||||
expect(getAdjacentNodes(nextState).size).toEqual(0);
|
||||
|
||||
nextState = reducer(nextState, ClickNodeAction);
|
||||
expect(getAdjacentNodes(nextState, 'n1').size).toEqual(2);
|
||||
expect(getAdjacentNodes(nextState, 'n1').has('n1')).toBeTruthy();
|
||||
expect(getAdjacentNodes(nextState, 'n1').has('n2')).toBeTruthy();
|
||||
|
||||
nextState = reducer(nextState, deSelectNode);
|
||||
expect(getAdjacentNodes(nextState).size).toEqual(0);
|
||||
});
|
||||
|
||||
// empty topology
|
||||
|
||||
it('detects that the topology is empty', () => {
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, ReceiveTopologiesAction);
|
||||
nextState = reducer(nextState, ClickTopologyAction);
|
||||
expect(isTopologyEmpty(nextState)).toBeFalsy();
|
||||
|
||||
nextState = reducer(nextState, ClickTopology2Action);
|
||||
expect(isTopologyEmpty(nextState)).toBeTruthy();
|
||||
|
||||
nextState = reducer(nextState, ClickTopologyAction);
|
||||
expect(isTopologyEmpty(nextState)).toBeFalsy();
|
||||
});
|
||||
|
||||
// selection of relatives
|
||||
|
||||
it('keeps relatives as a stack', () => {
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, ClickNodeAction);
|
||||
expect(nextState.get('selectedNodeId')).toBe('n1');
|
||||
expect(nextState.get('nodeDetails').size).toEqual(1);
|
||||
expect(nextState.get('nodeDetails').has('n1')).toBeTruthy();
|
||||
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('n1');
|
||||
|
||||
nextState = reducer(nextState, ClickRelativeAction);
|
||||
// stack relative, first node stays main node
|
||||
expect(nextState.get('selectedNodeId')).toBe('n1');
|
||||
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('rel1');
|
||||
expect(nextState.get('nodeDetails').size).toEqual(2);
|
||||
expect(nextState.get('nodeDetails').has('rel1')).toBeTruthy();
|
||||
|
||||
// click on first node should clear the stack
|
||||
nextState = reducer(nextState, ClickNodeAction);
|
||||
expect(nextState.get('selectedNodeId')).toBe('n1');
|
||||
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('n1');
|
||||
expect(nextState.get('nodeDetails').size).toEqual(1);
|
||||
expect(nextState.get('nodeDetails').has('rel1')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('keeps clears stack when sibling is clicked', () => {
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, ClickNodeAction);
|
||||
expect(nextState.get('selectedNodeId')).toBe('n1');
|
||||
expect(nextState.get('nodeDetails').size).toEqual(1);
|
||||
expect(nextState.get('nodeDetails').has('n1')).toBeTruthy();
|
||||
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('n1');
|
||||
|
||||
nextState = reducer(nextState, ClickRelativeAction);
|
||||
// stack relative, first node stays main node
|
||||
expect(nextState.get('selectedNodeId')).toBe('n1');
|
||||
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('rel1');
|
||||
expect(nextState.get('nodeDetails').size).toEqual(2);
|
||||
expect(nextState.get('nodeDetails').has('rel1')).toBeTruthy();
|
||||
|
||||
// click on sibling node should clear the stack
|
||||
nextState = reducer(nextState, ClickNode2Action);
|
||||
expect(nextState.get('selectedNodeId')).toBe('n2');
|
||||
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('n2');
|
||||
expect(nextState.get('nodeDetails').size).toEqual(1);
|
||||
expect(nextState.get('nodeDetails').has('n1')).toBeFalsy();
|
||||
expect(nextState.get('nodeDetails').has('rel1')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('selectes relatives topology while keeping node selected', () => {
|
||||
let nextState = initialState;
|
||||
nextState = reducer(nextState, ClickTopologyAction);
|
||||
nextState = reducer(nextState, ReceiveTopologiesAction);
|
||||
expect(nextState.get('currentTopology').get('name')).toBe('Topo1');
|
||||
|
||||
nextState = reducer(nextState, ClickNodeAction);
|
||||
expect(nextState.get('selectedNodeId')).toBe('n1');
|
||||
expect(nextState.get('nodeDetails').size).toEqual(1);
|
||||
expect(nextState.get('nodeDetails').has('n1')).toBeTruthy();
|
||||
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('n1');
|
||||
|
||||
nextState = reducer(nextState, ClickRelativeAction);
|
||||
// stack relative, first node stays main node
|
||||
expect(nextState.get('selectedNodeId')).toBe('n1');
|
||||
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('rel1');
|
||||
expect(nextState.get('nodeDetails').size).toEqual(2);
|
||||
expect(nextState.get('nodeDetails').has('rel1')).toBeTruthy();
|
||||
|
||||
// click switches over to relative's topology and selectes relative
|
||||
nextState = reducer(nextState, ClickShowTopologyForNodeAction);
|
||||
expect(nextState.get('selectedNodeId')).toBe('rel1');
|
||||
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('rel1');
|
||||
expect(nextState.get('nodeDetails').size).toEqual(1);
|
||||
expect(nextState.get('currentTopology').get('name')).toBe('Topo2');
|
||||
});
|
||||
});
|
||||
560
client/app/scripts/reducers/root.js
Normal file
560
client/app/scripts/reducers/root.js
Normal file
@@ -0,0 +1,560 @@
|
||||
import _ from 'lodash';
|
||||
import debug from 'debug';
|
||||
import { fromJS, is as isDeepEqual, List as makeList, Map as makeMap,
|
||||
OrderedMap as makeOrderedMap, Set as makeSet } from 'immutable';
|
||||
|
||||
import ActionTypes from '../constants/action-types';
|
||||
import { EDGE_ID_SEPARATOR } from '../constants/naming';
|
||||
import { findTopologyById, getAdjacentNodes, setTopologyUrlsById,
|
||||
updateTopologyIds, filterHiddenTopologies } from '../utils/topology-utils';
|
||||
|
||||
const log = debug('scope:app-store');
|
||||
const error = debug('scope:error');
|
||||
|
||||
// Helpers
|
||||
|
||||
function makeNode(node) {
|
||||
return {
|
||||
id: node.id,
|
||||
label: node.label,
|
||||
label_minor: node.label_minor,
|
||||
node_count: node.node_count,
|
||||
rank: node.rank,
|
||||
pseudo: node.pseudo,
|
||||
stack: node.stack,
|
||||
shape: node.shape,
|
||||
adjacency: node.adjacency,
|
||||
metrics: node.metrics
|
||||
};
|
||||
}
|
||||
|
||||
const topologySorter = topology => topology.get('rank');
|
||||
|
||||
// Initial values
|
||||
|
||||
export const initialState = makeMap({
|
||||
topologyOptions: makeOrderedMap(), // topologyId -> options
|
||||
controlStatus: makeMap(),
|
||||
currentTopology: null,
|
||||
currentTopologyId: 'containers',
|
||||
errorUrl: null,
|
||||
forceRelayout: false,
|
||||
highlightedEdgeIds: makeSet(),
|
||||
highlightedNodeIds: makeSet(),
|
||||
hostname: '...',
|
||||
version: '...',
|
||||
versionUpdate: null,
|
||||
plugins: makeList(),
|
||||
mouseOverEdgeId: null,
|
||||
mouseOverNodeId: null,
|
||||
nodeDetails: makeOrderedMap(), // nodeId -> details
|
||||
nodes: makeOrderedMap(), // nodeId -> node
|
||||
selectedNodeId: null,
|
||||
topologies: makeList(),
|
||||
topologiesLoaded: false,
|
||||
topologyUrlsById: makeOrderedMap(), // topologyId -> topologyUrl
|
||||
routeSet: false,
|
||||
controlPipes: makeOrderedMap(), // pipeId -> controlPipe
|
||||
updatePausedAt: null, // Date
|
||||
websocketClosed: true,
|
||||
showingHelp: false,
|
||||
|
||||
selectedMetric: null,
|
||||
pinnedMetric: null,
|
||||
// class of metric, e.g. 'cpu', rather than 'host_cpu' or 'process_cpu'.
|
||||
// allows us to keep the same metric "type" selected when the topology changes.
|
||||
pinnedMetricType: null,
|
||||
availableCanvasMetrics: makeList()
|
||||
});
|
||||
|
||||
// adds ID field to topology (based on last part of URL path) and save urls in
|
||||
// map for easy lookup
|
||||
function processTopologies(state, nextTopologies) {
|
||||
// filter out hidden topos
|
||||
const visibleTopologies = filterHiddenTopologies(nextTopologies);
|
||||
|
||||
// add IDs to topology objects in-place
|
||||
const topologiesWithId = updateTopologyIds(visibleTopologies);
|
||||
|
||||
// cache URLs by ID
|
||||
state = state.set('topologyUrlsById',
|
||||
setTopologyUrlsById(state.get('topologyUrlsById'), topologiesWithId));
|
||||
|
||||
const immNextTopologies = fromJS(topologiesWithId).sortBy(topologySorter);
|
||||
return state.mergeDeepIn(['topologies'], immNextTopologies);
|
||||
}
|
||||
|
||||
function setTopology(state, topologyId) {
|
||||
state = state.set('currentTopology', findTopologyById(
|
||||
state.get('topologies'), topologyId));
|
||||
return state.set('currentTopologyId', topologyId);
|
||||
}
|
||||
|
||||
function setDefaultTopologyOptions(state, topologyList) {
|
||||
topologyList.forEach(topology => {
|
||||
let defaultOptions = makeOrderedMap();
|
||||
if (topology.has('options') && topology.get('options')) {
|
||||
topology.get('options').forEach((option) => {
|
||||
const optionId = option.get('id');
|
||||
const defaultValue = option.get('defaultValue');
|
||||
defaultOptions = defaultOptions.set(optionId, defaultValue);
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultOptions.size) {
|
||||
state = state.setIn(['topologyOptions', topology.get('id')],
|
||||
defaultOptions
|
||||
);
|
||||
}
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
function closeNodeDetails(state, nodeId) {
|
||||
const nodeDetails = state.get('nodeDetails');
|
||||
if (nodeDetails.size > 0) {
|
||||
const popNodeId = nodeId || nodeDetails.keySeq().last();
|
||||
// remove pipe if it belongs to the node being closed
|
||||
state = state.update('controlPipes',
|
||||
controlPipes => controlPipes.filter(pipe => pipe.get('nodeId') !== popNodeId));
|
||||
state = state.deleteIn(['nodeDetails', popNodeId]);
|
||||
}
|
||||
if (state.get('nodeDetails').size === 0 || state.get('selectedNodeId') === nodeId) {
|
||||
state = state.set('selectedNodeId', null);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function closeAllNodeDetails(state) {
|
||||
while (state.get('nodeDetails').size) {
|
||||
state = closeNodeDetails(state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function resumeUpdate(state) {
|
||||
return state.set('updatePausedAt', null);
|
||||
}
|
||||
|
||||
export function rootReducer(state = initialState, action) {
|
||||
if (!action.type) {
|
||||
error('Payload missing a type!', action);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case ActionTypes.CHANGE_TOPOLOGY_OPTION: {
|
||||
state = resumeUpdate(state);
|
||||
// set option on parent topology
|
||||
const topology = findTopologyById(state.get('topologies'), action.topologyId);
|
||||
if (topology) {
|
||||
const topologyId = topology.get('parentId') || topology.get('id');
|
||||
if (state.getIn(['topologyOptions', topologyId, action.option]) !== action.value) {
|
||||
state = state.update('nodes', nodes => nodes.clear());
|
||||
}
|
||||
state = state.setIn(
|
||||
['topologyOptions', topologyId, action.option],
|
||||
action.value
|
||||
);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.CLEAR_CONTROL_ERROR: {
|
||||
return state.removeIn(['controlStatus', action.nodeId, 'error']);
|
||||
}
|
||||
|
||||
case ActionTypes.CLICK_BACKGROUND: {
|
||||
return closeAllNodeDetails(state);
|
||||
}
|
||||
|
||||
case ActionTypes.CLICK_CLOSE_DETAILS: {
|
||||
return closeNodeDetails(state, action.nodeId);
|
||||
}
|
||||
|
||||
case ActionTypes.CLICK_CLOSE_TERMINAL: {
|
||||
return state.update('controlPipes', controlPipes => controlPipes.clear());
|
||||
}
|
||||
|
||||
case ActionTypes.CLICK_FORCE_RELAYOUT: {
|
||||
return state.set('forceRelayout', action.forceRelayout);
|
||||
}
|
||||
|
||||
case ActionTypes.CLICK_NODE: {
|
||||
const prevSelectedNodeId = state.get('selectedNodeId');
|
||||
const prevDetailsStackSize = state.get('nodeDetails').size;
|
||||
|
||||
// click on sibling closes all
|
||||
state = closeAllNodeDetails(state);
|
||||
|
||||
// select new node if it's not the same (in that case just delesect)
|
||||
if (prevDetailsStackSize > 1 || prevSelectedNodeId !== action.nodeId) {
|
||||
// dont set origin if a node was already selected, suppresses animation
|
||||
const origin = prevSelectedNodeId === null ? action.origin : null;
|
||||
state = state.setIn(['nodeDetails', action.nodeId],
|
||||
{
|
||||
id: action.nodeId,
|
||||
label: action.label,
|
||||
origin,
|
||||
topologyId: state.get('currentTopologyId')
|
||||
}
|
||||
);
|
||||
state = state.set('selectedNodeId', action.nodeId);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.CLICK_PAUSE_UPDATE: {
|
||||
return state.set('updatePausedAt', new Date);
|
||||
}
|
||||
|
||||
case ActionTypes.CLICK_RELATIVE: {
|
||||
if (state.hasIn(['nodeDetails', action.nodeId])) {
|
||||
// bring to front
|
||||
const details = state.getIn(['nodeDetails', action.nodeId]);
|
||||
state = state.deleteIn(['nodeDetails', action.nodeId]);
|
||||
state = state.setIn(['nodeDetails', action.nodeId], details);
|
||||
} else {
|
||||
state = state.setIn(['nodeDetails', action.nodeId],
|
||||
{
|
||||
id: action.nodeId,
|
||||
label: action.label,
|
||||
origin: action.origin,
|
||||
topologyId: action.topologyId
|
||||
}
|
||||
);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.CLICK_RESUME_UPDATE: {
|
||||
return resumeUpdate(state);
|
||||
}
|
||||
|
||||
case ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE: {
|
||||
state = resumeUpdate(state);
|
||||
|
||||
state = state.update('nodeDetails',
|
||||
nodeDetails => nodeDetails.filter((v, k) => k === action.nodeId));
|
||||
state = state.update('controlPipes', controlPipes => controlPipes.clear());
|
||||
state = state.set('selectedNodeId', action.nodeId);
|
||||
|
||||
if (action.topologyId !== state.get('currentTopologyId')) {
|
||||
state = setTopology(state, action.topologyId);
|
||||
state = state.update('nodes', nodes => nodes.clear());
|
||||
}
|
||||
state = state.set('availableCanvasMetrics', makeList());
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.CLICK_TOPOLOGY: {
|
||||
state = resumeUpdate(state);
|
||||
state = closeAllNodeDetails(state);
|
||||
|
||||
if (action.topologyId !== state.get('currentTopologyId')) {
|
||||
state = setTopology(state, action.topologyId);
|
||||
state = state.update('nodes', nodes => nodes.clear());
|
||||
}
|
||||
state = state.set('availableCanvasMetrics', makeList());
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.CLOSE_WEBSOCKET: {
|
||||
if (!state.get('websocketClosed')) {
|
||||
state = state.set('websocketClosed', true);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.SELECT_METRIC: {
|
||||
return state.set('selectedMetric', action.metricId);
|
||||
}
|
||||
|
||||
case ActionTypes.PIN_METRIC: {
|
||||
const metricTypes = makeMap(
|
||||
state.get('availableCanvasMetrics').map(m => [m.get('id'), m.get('label')]));
|
||||
return state.merge({
|
||||
pinnedMetric: action.metricId,
|
||||
pinnedMetricType: metricTypes.get(action.metricId),
|
||||
selectedMetric: action.metricId
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UNPIN_METRIC: {
|
||||
return state.merge({
|
||||
pinnedMetric: null,
|
||||
pinnedMetricType: null
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.SHOW_HELP: {
|
||||
return state.set('showingHelp', true);
|
||||
}
|
||||
|
||||
case ActionTypes.HIDE_HELP: {
|
||||
return state.set('showingHelp', false);
|
||||
}
|
||||
|
||||
case ActionTypes.DESELECT_NODE: {
|
||||
return closeNodeDetails(state);
|
||||
}
|
||||
|
||||
case ActionTypes.DO_CONTROL: {
|
||||
return state.setIn(['controlStatus', action.nodeId], makeMap({
|
||||
pending: true,
|
||||
error: null
|
||||
}));
|
||||
}
|
||||
|
||||
case ActionTypes.ENTER_EDGE: {
|
||||
// highlight adjacent nodes
|
||||
state = state.update('highlightedNodeIds', highlightedNodeIds => {
|
||||
highlightedNodeIds = highlightedNodeIds.clear();
|
||||
return highlightedNodeIds.union(action.edgeId.split(EDGE_ID_SEPARATOR));
|
||||
});
|
||||
|
||||
// highlight edge
|
||||
state = state.update('highlightedEdgeIds', highlightedEdgeIds => {
|
||||
highlightedEdgeIds = highlightedEdgeIds.clear();
|
||||
return highlightedEdgeIds.add(action.edgeId);
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.ENTER_NODE: {
|
||||
const nodeId = action.nodeId;
|
||||
const adjacentNodes = getAdjacentNodes(state, nodeId);
|
||||
|
||||
// highlight adjacent nodes
|
||||
state = state.update('highlightedNodeIds', highlightedNodeIds => {
|
||||
highlightedNodeIds = highlightedNodeIds.clear();
|
||||
highlightedNodeIds = highlightedNodeIds.add(nodeId);
|
||||
return highlightedNodeIds.union(adjacentNodes);
|
||||
});
|
||||
|
||||
// highlight edge
|
||||
state = state.update('highlightedEdgeIds', highlightedEdgeIds => {
|
||||
highlightedEdgeIds = highlightedEdgeIds.clear();
|
||||
if (adjacentNodes.size > 0) {
|
||||
// all neighbour combinations because we dont know which direction exists
|
||||
highlightedEdgeIds = highlightedEdgeIds.union(adjacentNodes.flatMap((adjacentId) => [
|
||||
[adjacentId, nodeId].join(EDGE_ID_SEPARATOR),
|
||||
[nodeId, adjacentId].join(EDGE_ID_SEPARATOR)
|
||||
]));
|
||||
}
|
||||
return highlightedEdgeIds;
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.LEAVE_EDGE: {
|
||||
state = state.update('highlightedEdgeIds', highlightedEdgeIds => highlightedEdgeIds.clear());
|
||||
state = state.update('highlightedNodeIds', highlightedNodeIds => highlightedNodeIds.clear());
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.LEAVE_NODE: {
|
||||
state = state.update('highlightedEdgeIds', highlightedEdgeIds => highlightedEdgeIds.clear());
|
||||
state = state.update('highlightedNodeIds', highlightedNodeIds => highlightedNodeIds.clear());
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.OPEN_WEBSOCKET: {
|
||||
// flush nodes cache after re-connect
|
||||
state = state.update('nodes', nodes => nodes.clear());
|
||||
state = state.set('websocketClosed', false);
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.DO_CONTROL_ERROR: {
|
||||
return state.setIn(['controlStatus', action.nodeId], makeMap({
|
||||
pending: false,
|
||||
error: action.error
|
||||
}));
|
||||
}
|
||||
case ActionTypes.DO_CONTROL_SUCCESS: {
|
||||
return state.setIn(['controlStatus', action.nodeId], makeMap({
|
||||
pending: false,
|
||||
error: null
|
||||
}));
|
||||
}
|
||||
|
||||
case ActionTypes.RECEIVE_CONTROL_NODE_REMOVED: {
|
||||
return closeNodeDetails(state, action.nodeId);
|
||||
}
|
||||
|
||||
case ActionTypes.RECEIVE_CONTROL_PIPE: {
|
||||
return state.setIn(['controlPipes', action.pipeId], makeOrderedMap({
|
||||
id: action.pipeId,
|
||||
nodeId: action.nodeId,
|
||||
raw: action.rawTty
|
||||
}));
|
||||
}
|
||||
|
||||
case ActionTypes.RECEIVE_CONTROL_PIPE_STATUS: {
|
||||
if (state.hasIn(['controlPipes', action.pipeId])) {
|
||||
state = state.setIn(['controlPipes', action.pipeId, 'status'], action.status);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.RECEIVE_ERROR: {
|
||||
if (state.get('errorUrl') !== null) {
|
||||
state = state.set('errorUrl', action.errorUrl);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.RECEIVE_NODE_DETAILS: {
|
||||
state = state.set('errorUrl', null);
|
||||
|
||||
// disregard if node is not selected anymore
|
||||
if (state.hasIn(['nodeDetails', action.details.id])) {
|
||||
state = state.updateIn(['nodeDetails', action.details.id], obj => {
|
||||
const result = Object.assign({}, obj);
|
||||
result.notFound = false;
|
||||
result.details = action.details;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.RECEIVE_NODES_DELTA: {
|
||||
const emptyMessage = !action.delta.add && !action.delta.remove
|
||||
&& !action.delta.update;
|
||||
|
||||
if (!emptyMessage) {
|
||||
log('RECEIVE_NODES_DELTA',
|
||||
'remove', _.size(action.delta.remove),
|
||||
'update', _.size(action.delta.update),
|
||||
'add', _.size(action.delta.add));
|
||||
}
|
||||
|
||||
state = state.set('errorUrl', null);
|
||||
|
||||
// nodes that no longer exist
|
||||
_.each(action.delta.remove, (nodeId) => {
|
||||
// in case node disappears before mouseleave event
|
||||
if (state.get('mouseOverNodeId') === nodeId) {
|
||||
state = state.set('mouseOverNodeId', null);
|
||||
}
|
||||
if (state.hasIn(['nodes', nodeId]) && _.includes(state.get('mouseOverEdgeId'), nodeId)) {
|
||||
state = state.set('mouseOverEdgeId', null);
|
||||
}
|
||||
state = state.deleteIn(['nodes', nodeId]);
|
||||
});
|
||||
|
||||
// update existing nodes
|
||||
_.each(action.delta.update, (node) => {
|
||||
if (state.hasIn(['nodes', node.id])) {
|
||||
state = state.updateIn(['nodes', node.id], n => n.merge(fromJS(node)));
|
||||
}
|
||||
});
|
||||
|
||||
// add new nodes
|
||||
_.each(action.delta.add, (node) => {
|
||||
state = state.setIn(['nodes', node.id], fromJS(makeNode(node)));
|
||||
});
|
||||
|
||||
state = state.set('availableCanvasMetrics', state.get('nodes')
|
||||
.valueSeq()
|
||||
.flatMap(n => (n.get('metrics') || makeList()).map(m => (
|
||||
makeMap({id: m.get('id'), label: m.get('label')})
|
||||
)))
|
||||
.toSet()
|
||||
.toList()
|
||||
.sortBy(m => m.get('label')));
|
||||
|
||||
const similarTypeMetric = state.get('availableCanvasMetrics')
|
||||
.find(m => m.get('label') === state.get('pinnedMetricType'));
|
||||
state = state.set('pinnedMetric', similarTypeMetric && similarTypeMetric.get('id'));
|
||||
// if something in the current topo is not already selected, select it.
|
||||
if (!state.get('availableCanvasMetrics')
|
||||
.map(m => m.get('id'))
|
||||
.toSet()
|
||||
.has(state.get('selectedMetric'))) {
|
||||
state = state.set('selectedMetric', state.get('pinnedMetric'));
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.RECEIVE_NOT_FOUND: {
|
||||
if (state.hasIn(['nodeDetails', action.nodeId])) {
|
||||
state = state.updateIn(['nodeDetails', action.nodeId], obj => {
|
||||
const result = Object.assign({}, obj);
|
||||
result.notFound = true;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.RECEIVE_TOPOLOGIES: {
|
||||
state = state.set('errorUrl', null);
|
||||
state = state.update('topologyUrlsById', topologyUrlsById => topologyUrlsById.clear());
|
||||
state = processTopologies(state, action.topologies);
|
||||
state = setTopology(state, state.get('currentTopologyId'));
|
||||
// only set on first load, if options are not already set via route
|
||||
if (!state.get('topologiesLoaded') && state.get('topologyOptions').size === 0) {
|
||||
state = setDefaultTopologyOptions(state, state.get('topologies'));
|
||||
}
|
||||
state = state.set('topologiesLoaded', true);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
case ActionTypes.RECEIVE_API_DETAILS: {
|
||||
state = state.set('errorUrl', null);
|
||||
|
||||
return state.merge({
|
||||
hostname: action.hostname,
|
||||
version: action.version,
|
||||
plugins: action.plugins,
|
||||
versionUpdate: action.newVersion
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.ROUTE_TOPOLOGY: {
|
||||
state = state.set('routeSet', true);
|
||||
if (state.get('currentTopologyId') !== action.state.topologyId) {
|
||||
state = state.update('nodes', nodes => nodes.clear());
|
||||
}
|
||||
state = setTopology(state, action.state.topologyId);
|
||||
state = setDefaultTopologyOptions(state, state.get('topologies'));
|
||||
state = state.merge({
|
||||
selectedNodeId: action.state.selectedNodeId,
|
||||
pinnedMetricType: action.state.pinnedMetricType
|
||||
});
|
||||
if (action.state.controlPipe) {
|
||||
state = state.set('controlPipes', makeOrderedMap({
|
||||
[action.state.controlPipe.id]:
|
||||
makeOrderedMap(action.state.controlPipe)
|
||||
}));
|
||||
} else {
|
||||
state = state.update('controlPipes', controlPipes => controlPipes.clear());
|
||||
}
|
||||
if (action.state.nodeDetails) {
|
||||
const actionNodeDetails = makeOrderedMap(
|
||||
action.state.nodeDetails.map(obj => [obj.id, obj]));
|
||||
// check if detail IDs have changed
|
||||
if (!isDeepEqual(state.get('nodeDetails').keySeq(), actionNodeDetails.keySeq())) {
|
||||
state = state.set('nodeDetails', actionNodeDetails);
|
||||
}
|
||||
} else {
|
||||
state = state.update('nodeDetails', nodeDetails => nodeDetails.clear());
|
||||
}
|
||||
state = state.set('topologyOptions',
|
||||
fromJS(action.state.topologyOptions) || state.get('topologyOptions'));
|
||||
return state;
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default rootReducer;
|
||||
@@ -1,434 +0,0 @@
|
||||
jest.dontMock('../../utils/topology-utils');
|
||||
jest.dontMock('../../constants/action-types');
|
||||
jest.dontMock('../app-store');
|
||||
|
||||
// Appstore test suite using Jasmine matchers
|
||||
|
||||
describe('AppStore', () => {
|
||||
const ActionTypes = require('../../constants/action-types').default;
|
||||
let AppStore;
|
||||
let registeredCallback;
|
||||
|
||||
// fixtures
|
||||
|
||||
const NODE_SET = {
|
||||
n1: {
|
||||
id: 'n1',
|
||||
rank: undefined,
|
||||
adjacency: ['n1', 'n2'],
|
||||
pseudo: undefined,
|
||||
label: undefined,
|
||||
label_minor: undefined
|
||||
},
|
||||
n2: {
|
||||
id: 'n2',
|
||||
rank: undefined,
|
||||
adjacency: undefined,
|
||||
pseudo: undefined,
|
||||
label: undefined,
|
||||
label_minor: undefined
|
||||
}
|
||||
};
|
||||
|
||||
// actions
|
||||
|
||||
const ChangeTopologyOptionAction = {
|
||||
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
|
||||
topologyId: 'topo1',
|
||||
option: 'option1',
|
||||
value: 'on'
|
||||
};
|
||||
|
||||
const ChangeTopologyOptionAction2 = {
|
||||
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
|
||||
topologyId: 'topo1',
|
||||
option: 'option1',
|
||||
value: 'off'
|
||||
};
|
||||
|
||||
const ClickNodeAction = {
|
||||
type: ActionTypes.CLICK_NODE,
|
||||
nodeId: 'n1'
|
||||
};
|
||||
|
||||
const ClickNode2Action = {
|
||||
type: ActionTypes.CLICK_NODE,
|
||||
nodeId: 'n2'
|
||||
};
|
||||
|
||||
const ClickRelativeAction = {
|
||||
type: ActionTypes.CLICK_RELATIVE,
|
||||
nodeId: 'rel1'
|
||||
};
|
||||
|
||||
const ClickShowTopologyForNodeAction = {
|
||||
type: ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE,
|
||||
topologyId: 'topo2',
|
||||
nodeId: 'rel1'
|
||||
};
|
||||
|
||||
const ClickSubTopologyAction = {
|
||||
type: ActionTypes.CLICK_TOPOLOGY,
|
||||
topologyId: 'topo1-grouped'
|
||||
};
|
||||
|
||||
const ClickTopologyAction = {
|
||||
type: ActionTypes.CLICK_TOPOLOGY,
|
||||
topologyId: 'topo1'
|
||||
};
|
||||
|
||||
const ClickTopology2Action = {
|
||||
type: ActionTypes.CLICK_TOPOLOGY,
|
||||
topologyId: 'topo2'
|
||||
};
|
||||
|
||||
const CloseWebsocketAction = {
|
||||
type: ActionTypes.CLOSE_WEBSOCKET
|
||||
};
|
||||
|
||||
const deSelectNode = {
|
||||
type: ActionTypes.DESELECT_NODE
|
||||
};
|
||||
|
||||
const OpenWebsocketAction = {
|
||||
type: ActionTypes.OPEN_WEBSOCKET
|
||||
};
|
||||
|
||||
const ReceiveNodesDeltaAction = {
|
||||
type: ActionTypes.RECEIVE_NODES_DELTA,
|
||||
delta: {
|
||||
add: [{
|
||||
id: 'n1',
|
||||
adjacency: ['n1', 'n2']
|
||||
}, {
|
||||
id: 'n2'
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const ReceiveNodesDeltaUpdateAction = {
|
||||
type: ActionTypes.RECEIVE_NODES_DELTA,
|
||||
delta: {
|
||||
update: [{
|
||||
id: 'n1',
|
||||
adjacency: ['n1']
|
||||
}],
|
||||
remove: ['n2']
|
||||
}
|
||||
};
|
||||
|
||||
const ReceiveTopologiesAction = {
|
||||
type: ActionTypes.RECEIVE_TOPOLOGIES,
|
||||
topologies: [{
|
||||
url: '/topo1',
|
||||
name: 'Topo1',
|
||||
options: [{
|
||||
id: 'option1',
|
||||
defaultValue: 'off',
|
||||
options: [
|
||||
{value: 'on'},
|
||||
{value: 'off'}
|
||||
]
|
||||
}],
|
||||
stats: {
|
||||
node_count: 1
|
||||
},
|
||||
sub_topologies: [{
|
||||
url: '/topo1-grouped',
|
||||
name: 'topo 1 grouped'
|
||||
}]
|
||||
}, {
|
||||
url: '/topo2',
|
||||
name: 'Topo2',
|
||||
stats: {
|
||||
node_count: 0
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
const RouteAction = {
|
||||
type: ActionTypes.ROUTE_TOPOLOGY,
|
||||
state: {}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
AppStore = require('../app-store').default;
|
||||
const AppDispatcher = AppStore.getDispatcher();
|
||||
const callback = AppDispatcher.dispatch.bind(AppDispatcher);
|
||||
registeredCallback = callback;
|
||||
});
|
||||
|
||||
// topology tests
|
||||
|
||||
it('init with no topologies', () => {
|
||||
const topos = AppStore.getTopologies();
|
||||
expect(topos.size).toBe(0);
|
||||
expect(AppStore.getCurrentTopology()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('get current topology', () => {
|
||||
registeredCallback(ReceiveTopologiesAction);
|
||||
registeredCallback(ClickTopologyAction);
|
||||
|
||||
expect(AppStore.getTopologies().size).toBe(2);
|
||||
expect(AppStore.getCurrentTopology().get('name')).toBe('Topo1');
|
||||
expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1');
|
||||
expect(AppStore.getCurrentTopologyOptions().first().get('id')).toBe('option1');
|
||||
});
|
||||
|
||||
it('get sub-topology', () => {
|
||||
registeredCallback(ReceiveTopologiesAction);
|
||||
registeredCallback(ClickSubTopologyAction);
|
||||
|
||||
expect(AppStore.getTopologies().size).toBe(2);
|
||||
expect(AppStore.getCurrentTopology().get('name')).toBe('topo 1 grouped');
|
||||
expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1-grouped');
|
||||
expect(AppStore.getCurrentTopologyOptions().size).toBe(0);
|
||||
});
|
||||
|
||||
// topology options
|
||||
|
||||
it('changes topology option', () => {
|
||||
// default options
|
||||
registeredCallback(ReceiveTopologiesAction);
|
||||
registeredCallback(ClickTopologyAction);
|
||||
expect(AppStore.getActiveTopologyOptions().has('option1')).toBeTruthy();
|
||||
expect(AppStore.getActiveTopologyOptions().get('option1')).toBe('off');
|
||||
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('off');
|
||||
|
||||
// turn on
|
||||
registeredCallback(ChangeTopologyOptionAction);
|
||||
expect(AppStore.getActiveTopologyOptions().get('option1')).toBe('on');
|
||||
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('on');
|
||||
|
||||
// turn off
|
||||
registeredCallback(ChangeTopologyOptionAction2);
|
||||
expect(AppStore.getActiveTopologyOptions().get('option1')).toBe('off');
|
||||
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('off');
|
||||
|
||||
// sub-topology should retain main topo options
|
||||
registeredCallback(ClickSubTopologyAction);
|
||||
expect(AppStore.getActiveTopologyOptions().get('option1')).toBe('off');
|
||||
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('off');
|
||||
|
||||
// other topology w/o options dont return options, but keep in app state
|
||||
registeredCallback(ClickTopology2Action);
|
||||
expect(AppStore.getActiveTopologyOptions()).toBeUndefined();
|
||||
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('off');
|
||||
});
|
||||
|
||||
it('sets topology options from route', () => {
|
||||
RouteAction.state = {
|
||||
topologyId: 'topo1',
|
||||
selectedNodeId: null,
|
||||
topologyOptions: {topo1: {option1: 'on'}}};
|
||||
registeredCallback(RouteAction);
|
||||
expect(AppStore.getActiveTopologyOptions().get('option1')).toBe('on');
|
||||
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('on');
|
||||
|
||||
// stay same after topos have been received
|
||||
registeredCallback(ReceiveTopologiesAction);
|
||||
registeredCallback(ClickTopologyAction);
|
||||
expect(AppStore.getActiveTopologyOptions().get('option1')).toBe('on');
|
||||
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('on');
|
||||
});
|
||||
|
||||
it('uses default topology options from route', () => {
|
||||
RouteAction.state = {
|
||||
topologyId: 'topo1',
|
||||
selectedNodeId: null,
|
||||
topologyOptions: null};
|
||||
registeredCallback(RouteAction);
|
||||
registeredCallback(ReceiveTopologiesAction);
|
||||
registeredCallback(ClickTopologyAction);
|
||||
expect(AppStore.getActiveTopologyOptions().get('option1')).toBe('off');
|
||||
expect(AppStore.getAppState().topologyOptions.topo1.option1).toBe('off');
|
||||
});
|
||||
|
||||
// nodes delta
|
||||
|
||||
it('replaces adjacency on update', () => {
|
||||
registeredCallback(ReceiveNodesDeltaAction);
|
||||
expect(AppStore.getNodes().toJS().n1.adjacency).toEqual(['n1', 'n2']);
|
||||
registeredCallback(ReceiveNodesDeltaUpdateAction);
|
||||
expect(AppStore.getNodes().toJS().n1.adjacency).toEqual(['n1']);
|
||||
});
|
||||
|
||||
// browsing
|
||||
|
||||
it('shows nodes that were received', () => {
|
||||
registeredCallback(ReceiveNodesDeltaAction);
|
||||
expect(AppStore.getNodes().toJS()).toEqual(NODE_SET);
|
||||
});
|
||||
|
||||
it('knows a route was set', () => {
|
||||
expect(AppStore.isRouteSet()).toBeFalsy();
|
||||
registeredCallback(RouteAction);
|
||||
expect(AppStore.isRouteSet()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('gets selected node after click', () => {
|
||||
registeredCallback(ReceiveNodesDeltaAction);
|
||||
|
||||
registeredCallback(ClickNodeAction);
|
||||
expect(AppStore.getSelectedNodeId()).toBe('n1');
|
||||
expect(AppStore.getNodes().toJS()).toEqual(NODE_SET);
|
||||
|
||||
registeredCallback(deSelectNode);
|
||||
expect(AppStore.getSelectedNodeId()).toBe(null);
|
||||
expect(AppStore.getNodes().toJS()).toEqual(NODE_SET);
|
||||
});
|
||||
|
||||
it('keeps showing nodes on navigating back after node click', () => {
|
||||
registeredCallback(ReceiveTopologiesAction);
|
||||
registeredCallback(ClickTopologyAction);
|
||||
registeredCallback(ReceiveNodesDeltaAction);
|
||||
|
||||
expect(AppStore.getAppState().selectedNodeId).toEqual(null);
|
||||
|
||||
registeredCallback(ClickNodeAction);
|
||||
expect(AppStore.getAppState().selectedNodeId).toEqual('n1');
|
||||
|
||||
// go back in browsing
|
||||
RouteAction.state = {topologyId: 'topo1', selectedNodeId: null};
|
||||
registeredCallback(RouteAction);
|
||||
expect(AppStore.getSelectedNodeId()).toBe(null);
|
||||
expect(AppStore.getNodes().toJS()).toEqual(NODE_SET);
|
||||
});
|
||||
|
||||
it('closes details when changing topologies', () => {
|
||||
registeredCallback(ReceiveTopologiesAction);
|
||||
registeredCallback(ClickTopologyAction);
|
||||
registeredCallback(ReceiveNodesDeltaAction);
|
||||
|
||||
expect(AppStore.getAppState().selectedNodeId).toEqual(null);
|
||||
expect(AppStore.getAppState().topologyId).toEqual('topo1');
|
||||
|
||||
registeredCallback(ClickNodeAction);
|
||||
expect(AppStore.getAppState().selectedNodeId).toEqual('n1');
|
||||
expect(AppStore.getAppState().topologyId).toEqual('topo1');
|
||||
|
||||
registeredCallback(ClickSubTopologyAction);
|
||||
expect(AppStore.getAppState().selectedNodeId).toEqual(null);
|
||||
expect(AppStore.getAppState().topologyId).toEqual('topo1-grouped');
|
||||
});
|
||||
|
||||
// connection errors
|
||||
|
||||
it('resets topology on websocket reconnect', () => {
|
||||
registeredCallback(ReceiveNodesDeltaAction);
|
||||
expect(AppStore.getNodes().toJS()).toEqual(NODE_SET);
|
||||
|
||||
registeredCallback(CloseWebsocketAction);
|
||||
expect(AppStore.isWebsocketClosed()).toBeTruthy();
|
||||
// keep showing old nodes
|
||||
expect(AppStore.getNodes().toJS()).toEqual(NODE_SET);
|
||||
|
||||
registeredCallback(OpenWebsocketAction);
|
||||
expect(AppStore.isWebsocketClosed()).toBeFalsy();
|
||||
// opened socket clears nodes
|
||||
expect(AppStore.getNodes().toJS()).toEqual({});
|
||||
});
|
||||
|
||||
// adjacency test
|
||||
|
||||
it('returns the correct adjacency set for a node', () => {
|
||||
registeredCallback(ReceiveNodesDeltaAction);
|
||||
expect(AppStore.getAdjacentNodes().size).toEqual(0);
|
||||
|
||||
registeredCallback(ClickNodeAction);
|
||||
expect(AppStore.getAdjacentNodes('n1').size).toEqual(2);
|
||||
expect(AppStore.getAdjacentNodes('n1').has('n1')).toBeTruthy();
|
||||
expect(AppStore.getAdjacentNodes('n1').has('n2')).toBeTruthy();
|
||||
|
||||
registeredCallback(deSelectNode);
|
||||
expect(AppStore.getAdjacentNodes().size).toEqual(0);
|
||||
});
|
||||
|
||||
// empty topology
|
||||
|
||||
it('detects that the topology is empty', () => {
|
||||
registeredCallback(ReceiveTopologiesAction);
|
||||
registeredCallback(ClickTopologyAction);
|
||||
expect(AppStore.isTopologyEmpty()).toBeFalsy();
|
||||
|
||||
registeredCallback(ClickTopology2Action);
|
||||
expect(AppStore.isTopologyEmpty()).toBeTruthy();
|
||||
|
||||
registeredCallback(ClickTopologyAction);
|
||||
expect(AppStore.isTopologyEmpty()).toBeFalsy();
|
||||
});
|
||||
|
||||
// selection of relatives
|
||||
|
||||
it('keeps relatives as a stack', () => {
|
||||
registeredCallback(ClickNodeAction);
|
||||
expect(AppStore.getSelectedNodeId()).toBe('n1');
|
||||
expect(AppStore.getNodeDetails().size).toEqual(1);
|
||||
expect(AppStore.getNodeDetails().has('n1')).toBeTruthy();
|
||||
expect(AppStore.getNodeDetails().keySeq().last()).toEqual('n1');
|
||||
|
||||
registeredCallback(ClickRelativeAction);
|
||||
// stack relative, first node stays main node
|
||||
expect(AppStore.getSelectedNodeId()).toBe('n1');
|
||||
expect(AppStore.getNodeDetails().keySeq().last()).toEqual('rel1');
|
||||
expect(AppStore.getNodeDetails().size).toEqual(2);
|
||||
expect(AppStore.getNodeDetails().has('rel1')).toBeTruthy();
|
||||
|
||||
// click on first node should clear the stack
|
||||
registeredCallback(ClickNodeAction);
|
||||
expect(AppStore.getSelectedNodeId()).toBe('n1');
|
||||
expect(AppStore.getNodeDetails().keySeq().last()).toEqual('n1');
|
||||
expect(AppStore.getNodeDetails().size).toEqual(1);
|
||||
expect(AppStore.getNodeDetails().has('rel1')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('keeps clears stack when sibling is clicked', () => {
|
||||
registeredCallback(ClickNodeAction);
|
||||
expect(AppStore.getSelectedNodeId()).toBe('n1');
|
||||
expect(AppStore.getNodeDetails().size).toEqual(1);
|
||||
expect(AppStore.getNodeDetails().has('n1')).toBeTruthy();
|
||||
expect(AppStore.getNodeDetails().keySeq().last()).toEqual('n1');
|
||||
|
||||
registeredCallback(ClickRelativeAction);
|
||||
// stack relative, first node stays main node
|
||||
expect(AppStore.getSelectedNodeId()).toBe('n1');
|
||||
expect(AppStore.getNodeDetails().keySeq().last()).toEqual('rel1');
|
||||
expect(AppStore.getNodeDetails().size).toEqual(2);
|
||||
expect(AppStore.getNodeDetails().has('rel1')).toBeTruthy();
|
||||
|
||||
// click on sibling node should clear the stack
|
||||
registeredCallback(ClickNode2Action);
|
||||
expect(AppStore.getSelectedNodeId()).toBe('n2');
|
||||
expect(AppStore.getNodeDetails().keySeq().last()).toEqual('n2');
|
||||
expect(AppStore.getNodeDetails().size).toEqual(1);
|
||||
expect(AppStore.getNodeDetails().has('n1')).toBeFalsy();
|
||||
expect(AppStore.getNodeDetails().has('rel1')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('selectes relatives topology while keeping node selected', () => {
|
||||
registeredCallback(ClickTopologyAction);
|
||||
registeredCallback(ReceiveTopologiesAction);
|
||||
expect(AppStore.getCurrentTopology().get('name')).toBe('Topo1');
|
||||
|
||||
registeredCallback(ClickNodeAction);
|
||||
expect(AppStore.getSelectedNodeId()).toBe('n1');
|
||||
expect(AppStore.getNodeDetails().size).toEqual(1);
|
||||
expect(AppStore.getNodeDetails().has('n1')).toBeTruthy();
|
||||
expect(AppStore.getNodeDetails().keySeq().last()).toEqual('n1');
|
||||
|
||||
registeredCallback(ClickRelativeAction);
|
||||
// stack relative, first node stays main node
|
||||
expect(AppStore.getSelectedNodeId()).toBe('n1');
|
||||
expect(AppStore.getNodeDetails().keySeq().last()).toEqual('rel1');
|
||||
expect(AppStore.getNodeDetails().size).toEqual(2);
|
||||
expect(AppStore.getNodeDetails().has('rel1')).toBeTruthy();
|
||||
|
||||
// click switches over to relative's topology and selectes relative
|
||||
registeredCallback(ClickShowTopologyForNodeAction);
|
||||
expect(AppStore.getSelectedNodeId()).toBe('rel1');
|
||||
expect(AppStore.getNodeDetails().keySeq().last()).toEqual('rel1');
|
||||
expect(AppStore.getNodeDetails().size).toEqual(1);
|
||||
expect(AppStore.getCurrentTopology().get('name')).toBe('Topo2');
|
||||
});
|
||||
});
|
||||
@@ -1,741 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import debug from 'debug';
|
||||
import { fromJS, is as isDeepEqual, List, Map, OrderedMap, Set } from 'immutable';
|
||||
import { Store } from 'flux/utils';
|
||||
|
||||
import AppDispatcher from '../dispatcher/app-dispatcher';
|
||||
import ActionTypes from '../constants/action-types';
|
||||
import { EDGE_ID_SEPARATOR } from '../constants/naming';
|
||||
import { findTopologyById, setTopologyUrlsById, updateTopologyIds,
|
||||
filterHiddenTopologies } from '../utils/topology-utils';
|
||||
|
||||
const makeList = List;
|
||||
const makeMap = Map;
|
||||
const makeOrderedMap = OrderedMap;
|
||||
const makeSet = Set;
|
||||
const log = debug('scope:app-store');
|
||||
|
||||
const error = debug('scope:error');
|
||||
|
||||
// Helpers
|
||||
|
||||
function makeNode(node) {
|
||||
return {
|
||||
id: node.id,
|
||||
label: node.label,
|
||||
label_minor: node.label_minor,
|
||||
node_count: node.node_count,
|
||||
rank: node.rank,
|
||||
pseudo: node.pseudo,
|
||||
stack: node.stack,
|
||||
shape: node.shape,
|
||||
adjacency: node.adjacency,
|
||||
metrics: node.metrics
|
||||
};
|
||||
}
|
||||
|
||||
// Initial values
|
||||
|
||||
let topologyOptions = makeOrderedMap(); // topologyId -> options
|
||||
let controlStatus = makeMap();
|
||||
let currentTopology = null;
|
||||
let currentTopologyId = 'containers';
|
||||
let errorUrl = null;
|
||||
let forceRelayout = false;
|
||||
let highlightedEdgeIds = makeSet();
|
||||
let highlightedNodeIds = makeSet();
|
||||
let hostname = '...';
|
||||
let version = '...';
|
||||
let versionUpdate = null;
|
||||
let plugins = [];
|
||||
let mouseOverEdgeId = null;
|
||||
let mouseOverNodeId = null;
|
||||
let nodeDetails = makeOrderedMap(); // nodeId -> details
|
||||
let nodes = makeOrderedMap(); // nodeId -> node
|
||||
let selectedNodeId = null;
|
||||
let topologies = makeList();
|
||||
let topologiesLoaded = false;
|
||||
let topologyUrlsById = makeOrderedMap(); // topologyId -> topologyUrl
|
||||
let routeSet = false;
|
||||
let controlPipes = makeOrderedMap(); // pipeId -> controlPipe
|
||||
let updatePausedAt = null; // Date
|
||||
let websocketClosed = true;
|
||||
let showingHelp = false;
|
||||
|
||||
let selectedMetric = null;
|
||||
let pinnedMetric = selectedMetric;
|
||||
// class of metric, e.g. 'cpu', rather than 'host_cpu' or 'process_cpu'.
|
||||
// allows us to keep the same metric "type" selected when the topology changes.
|
||||
let pinnedMetricType = null;
|
||||
let availableCanvasMetrics = makeList();
|
||||
|
||||
|
||||
const topologySorter = topology => topology.get('rank');
|
||||
|
||||
// adds ID field to topology (based on last part of URL path) and save urls in
|
||||
// map for easy lookup
|
||||
function processTopologies(nextTopologies) {
|
||||
// filter out hidden topos
|
||||
const visibleTopologies = filterHiddenTopologies(nextTopologies);
|
||||
|
||||
// add IDs to topology objects in-place
|
||||
const topologiesWithId = updateTopologyIds(visibleTopologies);
|
||||
|
||||
// cache URLs by ID
|
||||
topologyUrlsById = setTopologyUrlsById(topologyUrlsById, topologiesWithId);
|
||||
|
||||
const immNextTopologies = fromJS(topologiesWithId).sortBy(topologySorter);
|
||||
topologies = topologies.mergeDeep(immNextTopologies);
|
||||
}
|
||||
|
||||
function setTopology(topologyId) {
|
||||
currentTopology = findTopologyById(topologies, topologyId);
|
||||
currentTopologyId = topologyId;
|
||||
}
|
||||
|
||||
function setDefaultTopologyOptions(topologyList) {
|
||||
topologyList.forEach(topology => {
|
||||
let defaultOptions = makeOrderedMap();
|
||||
if (topology.has('options') && topology.get('options')) {
|
||||
topology.get('options').forEach((option) => {
|
||||
const optionId = option.get('id');
|
||||
const defaultValue = option.get('defaultValue');
|
||||
defaultOptions = defaultOptions.set(optionId, defaultValue);
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultOptions.size) {
|
||||
topologyOptions = topologyOptions.set(
|
||||
topology.get('id'),
|
||||
defaultOptions
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeNodeDetails(nodeId) {
|
||||
if (nodeDetails.size > 0) {
|
||||
const popNodeId = nodeId || nodeDetails.keySeq().last();
|
||||
// remove pipe if it belongs to the node being closed
|
||||
controlPipes = controlPipes.filter(pipe => pipe.get('nodeId') !== popNodeId);
|
||||
nodeDetails = nodeDetails.delete(popNodeId);
|
||||
}
|
||||
if (nodeDetails.size === 0 || selectedNodeId === nodeId) {
|
||||
selectedNodeId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function closeAllNodeDetails() {
|
||||
while (nodeDetails.size) {
|
||||
closeNodeDetails();
|
||||
}
|
||||
}
|
||||
|
||||
function resumeUpdate() {
|
||||
updatePausedAt = null;
|
||||
}
|
||||
|
||||
// Store API
|
||||
|
||||
export class AppStore extends Store {
|
||||
|
||||
// keep at the top
|
||||
getAppState() {
|
||||
const cp = this.getControlPipe();
|
||||
return {
|
||||
controlPipe: cp ? cp.toJS() : null,
|
||||
nodeDetails: this.getNodeDetailsState().toJS(),
|
||||
selectedNodeId,
|
||||
pinnedMetricType,
|
||||
topologyId: currentTopologyId,
|
||||
topologyOptions: topologyOptions.toJS() // all options
|
||||
};
|
||||
}
|
||||
|
||||
getShowingHelp() {
|
||||
return showingHelp;
|
||||
}
|
||||
|
||||
getActiveTopologyOptions() {
|
||||
// options for current topology, sub-topologies share options with parent
|
||||
if (currentTopology && currentTopology.get('parentId')) {
|
||||
return topologyOptions.get(currentTopology.get('parentId'));
|
||||
}
|
||||
return topologyOptions.get(currentTopologyId);
|
||||
}
|
||||
|
||||
getAdjacentNodes(nodeId) {
|
||||
let adjacentNodes = makeSet();
|
||||
|
||||
if (nodes.has(nodeId)) {
|
||||
adjacentNodes = makeSet(nodes.getIn([nodeId, 'adjacency']));
|
||||
// fill up set with reverse edges
|
||||
nodes.forEach((node, id) => {
|
||||
if (node.get('adjacency') && node.get('adjacency').includes(nodeId)) {
|
||||
adjacentNodes = adjacentNodes.add(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return adjacentNodes;
|
||||
}
|
||||
|
||||
getPinnedMetric() {
|
||||
return pinnedMetric;
|
||||
}
|
||||
|
||||
getSelectedMetric() {
|
||||
return selectedMetric;
|
||||
}
|
||||
|
||||
getAvailableCanvasMetrics() {
|
||||
return availableCanvasMetrics;
|
||||
}
|
||||
|
||||
getAvailableCanvasMetricsTypes() {
|
||||
return makeMap(this.getAvailableCanvasMetrics().map(m => [m.get('id'), m.get('label')]));
|
||||
}
|
||||
|
||||
getControlStatus() {
|
||||
return controlStatus;
|
||||
}
|
||||
|
||||
getControlPipe() {
|
||||
return controlPipes.last();
|
||||
}
|
||||
|
||||
getCurrentTopology() {
|
||||
if (!currentTopology) {
|
||||
currentTopology = setTopology(currentTopologyId);
|
||||
}
|
||||
return currentTopology;
|
||||
}
|
||||
|
||||
getCurrentTopologyId() {
|
||||
return currentTopologyId;
|
||||
}
|
||||
|
||||
getCurrentTopologyOptions() {
|
||||
return currentTopology && currentTopology.get('options') || makeOrderedMap();
|
||||
}
|
||||
|
||||
getCurrentTopologyUrl() {
|
||||
return currentTopology && currentTopology.get('url');
|
||||
}
|
||||
|
||||
getErrorUrl() {
|
||||
return errorUrl;
|
||||
}
|
||||
|
||||
getHighlightedEdgeIds() {
|
||||
return highlightedEdgeIds;
|
||||
}
|
||||
|
||||
getHighlightedNodeIds() {
|
||||
return highlightedNodeIds;
|
||||
}
|
||||
|
||||
getHostname() {
|
||||
return hostname;
|
||||
}
|
||||
|
||||
getNodeDetails() {
|
||||
return nodeDetails;
|
||||
}
|
||||
|
||||
getNodeDetailsState() {
|
||||
return nodeDetails.toIndexedSeq().map(details => ({
|
||||
id: details.id, label: details.label, topologyId: details.topologyId
|
||||
}));
|
||||
}
|
||||
|
||||
getTopCardNodeId() {
|
||||
return nodeDetails.last() && nodeDetails.last().id;
|
||||
}
|
||||
|
||||
getNodes() {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
getSelectedNodeId() {
|
||||
return selectedNodeId;
|
||||
}
|
||||
|
||||
getTopologies() {
|
||||
return topologies;
|
||||
}
|
||||
|
||||
getTopologyUrlsById() {
|
||||
return topologyUrlsById;
|
||||
}
|
||||
|
||||
getUpdatePausedAt() {
|
||||
return updatePausedAt;
|
||||
}
|
||||
|
||||
getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
getVersionUpdate() {
|
||||
return versionUpdate;
|
||||
}
|
||||
|
||||
getPlugins() {
|
||||
return plugins;
|
||||
}
|
||||
|
||||
isForceRelayout() {
|
||||
return forceRelayout;
|
||||
}
|
||||
|
||||
isRouteSet() {
|
||||
return routeSet;
|
||||
}
|
||||
|
||||
isTopologiesLoaded() {
|
||||
return topologiesLoaded;
|
||||
}
|
||||
|
||||
isTopologyEmpty() {
|
||||
return currentTopology && currentTopology.get('stats')
|
||||
&& currentTopology.get('stats').get('node_count') === 0 && nodes.size === 0;
|
||||
}
|
||||
|
||||
isUpdatePaused() {
|
||||
return updatePausedAt !== null;
|
||||
}
|
||||
|
||||
isWebsocketClosed() {
|
||||
return websocketClosed;
|
||||
}
|
||||
|
||||
__onDispatch(payload) {
|
||||
if (!payload.type) {
|
||||
error('Payload missing a type!', payload);
|
||||
}
|
||||
|
||||
switch (payload.type) {
|
||||
case ActionTypes.CHANGE_TOPOLOGY_OPTION: {
|
||||
resumeUpdate();
|
||||
// set option on parent topology
|
||||
const topology = findTopologyById(topologies, payload.topologyId);
|
||||
if (topology) {
|
||||
const topologyId = topology.get('parentId') || topology.get('id');
|
||||
if (topologyOptions.getIn([topologyId, payload.option]) !== payload.value) {
|
||||
nodes = nodes.clear();
|
||||
}
|
||||
topologyOptions = topologyOptions.setIn(
|
||||
[topologyId, payload.option],
|
||||
payload.value
|
||||
);
|
||||
this.__emitChange();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLEAR_CONTROL_ERROR: {
|
||||
controlStatus = controlStatus.removeIn([payload.nodeId, 'error']);
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_BACKGROUND: {
|
||||
closeAllNodeDetails();
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_CLOSE_DETAILS: {
|
||||
closeNodeDetails(payload.nodeId);
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_CLOSE_TERMINAL: {
|
||||
controlPipes = controlPipes.clear();
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_FORCE_RELAYOUT: {
|
||||
forceRelayout = true;
|
||||
// fire only once, reset after emitChange
|
||||
setTimeout(() => {
|
||||
forceRelayout = false;
|
||||
}, 0);
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_NODE: {
|
||||
const prevSelectedNodeId = selectedNodeId;
|
||||
const prevDetailsStackSize = nodeDetails.size;
|
||||
// click on sibling closes all
|
||||
closeAllNodeDetails();
|
||||
// select new node if it's not the same (in that case just delesect)
|
||||
if (prevDetailsStackSize > 1 || prevSelectedNodeId !== payload.nodeId) {
|
||||
// dont set origin if a node was already selected, suppresses animation
|
||||
const origin = prevSelectedNodeId === null ? payload.origin : null;
|
||||
nodeDetails = nodeDetails.set(
|
||||
payload.nodeId,
|
||||
{
|
||||
id: payload.nodeId,
|
||||
label: payload.label,
|
||||
origin,
|
||||
topologyId: currentTopologyId
|
||||
}
|
||||
);
|
||||
selectedNodeId = payload.nodeId;
|
||||
}
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_PAUSE_UPDATE: {
|
||||
updatePausedAt = new Date;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_RELATIVE: {
|
||||
if (nodeDetails.has(payload.nodeId)) {
|
||||
// bring to front
|
||||
const details = nodeDetails.get(payload.nodeId);
|
||||
nodeDetails = nodeDetails.delete(payload.nodeId);
|
||||
nodeDetails = nodeDetails.set(payload.nodeId, details);
|
||||
} else {
|
||||
nodeDetails = nodeDetails.set(
|
||||
payload.nodeId,
|
||||
{
|
||||
id: payload.nodeId,
|
||||
label: payload.label,
|
||||
origin: payload.origin,
|
||||
topologyId: payload.topologyId
|
||||
}
|
||||
);
|
||||
}
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_RESUME_UPDATE: {
|
||||
resumeUpdate();
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE: {
|
||||
resumeUpdate();
|
||||
nodeDetails = nodeDetails.filter((v, k) => k === payload.nodeId);
|
||||
controlPipes = controlPipes.clear();
|
||||
selectedNodeId = payload.nodeId;
|
||||
if (payload.topologyId !== currentTopologyId) {
|
||||
setTopology(payload.topologyId);
|
||||
nodes = nodes.clear();
|
||||
}
|
||||
availableCanvasMetrics = makeList();
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_TOPOLOGY: {
|
||||
resumeUpdate();
|
||||
closeAllNodeDetails();
|
||||
if (payload.topologyId !== currentTopologyId) {
|
||||
setTopology(payload.topologyId);
|
||||
nodes = nodes.clear();
|
||||
}
|
||||
availableCanvasMetrics = makeList();
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLOSE_WEBSOCKET: {
|
||||
if (!websocketClosed) {
|
||||
websocketClosed = true;
|
||||
this.__emitChange();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActionTypes.SELECT_METRIC: {
|
||||
selectedMetric = payload.metricId;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.PIN_METRIC: {
|
||||
pinnedMetric = payload.metricId;
|
||||
pinnedMetricType = this.getAvailableCanvasMetricsTypes().get(payload.metricId);
|
||||
selectedMetric = payload.metricId;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.UNPIN_METRIC: {
|
||||
pinnedMetric = null;
|
||||
pinnedMetricType = null;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.SHOW_HELP: {
|
||||
showingHelp = true;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.HIDE_HELP: {
|
||||
showingHelp = false;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.DESELECT_NODE: {
|
||||
closeNodeDetails();
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.DO_CONTROL: {
|
||||
controlStatus = controlStatus.set(payload.nodeId, makeMap({
|
||||
pending: true,
|
||||
error: null
|
||||
}));
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.ENTER_EDGE: {
|
||||
// clear old highlights
|
||||
highlightedNodeIds = highlightedNodeIds.clear();
|
||||
highlightedEdgeIds = highlightedEdgeIds.clear();
|
||||
|
||||
// highlight edge
|
||||
highlightedEdgeIds = highlightedEdgeIds.add(payload.edgeId);
|
||||
|
||||
// highlight adjacent nodes
|
||||
highlightedNodeIds = highlightedNodeIds.union(payload.edgeId.split(EDGE_ID_SEPARATOR));
|
||||
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.ENTER_NODE: {
|
||||
const nodeId = payload.nodeId;
|
||||
const adjacentNodes = this.getAdjacentNodes(nodeId);
|
||||
|
||||
// clear old highlights
|
||||
highlightedNodeIds = highlightedNodeIds.clear();
|
||||
highlightedEdgeIds = highlightedEdgeIds.clear();
|
||||
|
||||
// highlight nodes
|
||||
highlightedNodeIds = highlightedNodeIds.add(nodeId);
|
||||
highlightedNodeIds = highlightedNodeIds.union(adjacentNodes);
|
||||
|
||||
// highlight edges
|
||||
if (adjacentNodes.size > 0) {
|
||||
// all neighbour combinations because we dont know which direction exists
|
||||
highlightedEdgeIds = highlightedEdgeIds.union(adjacentNodes.flatMap((adjacentId) => [
|
||||
[adjacentId, nodeId].join(EDGE_ID_SEPARATOR),
|
||||
[nodeId, adjacentId].join(EDGE_ID_SEPARATOR)
|
||||
]));
|
||||
}
|
||||
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.LEAVE_EDGE: {
|
||||
highlightedEdgeIds = highlightedEdgeIds.clear();
|
||||
highlightedNodeIds = highlightedNodeIds.clear();
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.LEAVE_NODE: {
|
||||
highlightedEdgeIds = highlightedEdgeIds.clear();
|
||||
highlightedNodeIds = highlightedNodeIds.clear();
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.OPEN_WEBSOCKET: {
|
||||
// flush nodes cache after re-connect
|
||||
nodes = nodes.clear();
|
||||
websocketClosed = false;
|
||||
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.DO_CONTROL_ERROR: {
|
||||
controlStatus = controlStatus.set(payload.nodeId, makeMap({
|
||||
pending: false,
|
||||
error: payload.error
|
||||
}));
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.DO_CONTROL_SUCCESS: {
|
||||
controlStatus = controlStatus.set(payload.nodeId, makeMap({
|
||||
pending: false,
|
||||
error: null
|
||||
}));
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_CONTROL_NODE_REMOVED: {
|
||||
closeNodeDetails(payload.nodeId);
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_CONTROL_PIPE: {
|
||||
controlPipes = controlPipes.set(payload.pipeId, makeOrderedMap({
|
||||
id: payload.pipeId,
|
||||
nodeId: payload.nodeId,
|
||||
raw: payload.rawTty
|
||||
}));
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_CONTROL_PIPE_STATUS: {
|
||||
if (controlPipes.has(payload.pipeId)) {
|
||||
controlPipes = controlPipes.setIn([payload.pipeId, 'status'], payload.status);
|
||||
this.__emitChange();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_ERROR: {
|
||||
if (errorUrl !== null) {
|
||||
errorUrl = payload.errorUrl;
|
||||
this.__emitChange();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_NODE_DETAILS: {
|
||||
errorUrl = null;
|
||||
|
||||
// disregard if node is not selected anymore
|
||||
if (nodeDetails.has(payload.details.id)) {
|
||||
nodeDetails = nodeDetails.update(payload.details.id, obj => {
|
||||
const result = Object.assign({}, obj);
|
||||
result.notFound = false;
|
||||
result.details = payload.details;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_NODES_DELTA: {
|
||||
const emptyMessage = !payload.delta.add && !payload.delta.remove
|
||||
&& !payload.delta.update;
|
||||
// this action is called frequently, good to check if something changed
|
||||
const emitChange = !emptyMessage || errorUrl !== null;
|
||||
|
||||
if (!emptyMessage) {
|
||||
log('RECEIVE_NODES_DELTA',
|
||||
'remove', _.size(payload.delta.remove),
|
||||
'update', _.size(payload.delta.update),
|
||||
'add', _.size(payload.delta.add));
|
||||
}
|
||||
|
||||
errorUrl = null;
|
||||
|
||||
// nodes that no longer exist
|
||||
_.each(payload.delta.remove, (nodeId) => {
|
||||
// in case node disappears before mouseleave event
|
||||
if (mouseOverNodeId === nodeId) {
|
||||
mouseOverNodeId = null;
|
||||
}
|
||||
if (nodes.has(nodeId) && _.includes(mouseOverEdgeId, nodeId)) {
|
||||
mouseOverEdgeId = null;
|
||||
}
|
||||
nodes = nodes.delete(nodeId);
|
||||
});
|
||||
|
||||
// update existing nodes
|
||||
_.each(payload.delta.update, (node) => {
|
||||
if (nodes.has(node.id)) {
|
||||
nodes = nodes.set(node.id, nodes.get(node.id).merge(fromJS(node)));
|
||||
}
|
||||
});
|
||||
|
||||
// add new nodes
|
||||
_.each(payload.delta.add, (node) => {
|
||||
nodes = nodes.set(node.id, fromJS(makeNode(node)));
|
||||
});
|
||||
|
||||
availableCanvasMetrics = nodes
|
||||
.valueSeq()
|
||||
.flatMap(n => (n.get('metrics') || makeList()).map(m => (
|
||||
makeMap({id: m.get('id'), label: m.get('label')})
|
||||
)))
|
||||
.toSet()
|
||||
.toList()
|
||||
.sortBy(m => m.get('label'));
|
||||
|
||||
const similarTypeMetric = availableCanvasMetrics
|
||||
.find(m => m.get('label') === pinnedMetricType);
|
||||
pinnedMetric = similarTypeMetric && similarTypeMetric.get('id');
|
||||
// if something in the current topo is not already selected, select it.
|
||||
if (!availableCanvasMetrics.map(m => m.get('id')).toSet().has(selectedMetric)) {
|
||||
selectedMetric = pinnedMetric;
|
||||
}
|
||||
|
||||
if (emitChange) {
|
||||
this.__emitChange();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_NOT_FOUND: {
|
||||
if (nodeDetails.has(payload.nodeId)) {
|
||||
nodeDetails = nodeDetails.update(payload.nodeId, obj => {
|
||||
const result = Object.assign({}, obj);
|
||||
result.notFound = true;
|
||||
return result;
|
||||
});
|
||||
this.__emitChange();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_TOPOLOGIES: {
|
||||
errorUrl = null;
|
||||
topologyUrlsById = topologyUrlsById.clear();
|
||||
processTopologies(payload.topologies);
|
||||
setTopology(currentTopologyId);
|
||||
// only set on first load, if options are not already set via route
|
||||
if (!topologiesLoaded && topologyOptions.size === 0) {
|
||||
setDefaultTopologyOptions(topologies);
|
||||
}
|
||||
topologiesLoaded = true;
|
||||
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_API_DETAILS: {
|
||||
errorUrl = null;
|
||||
hostname = payload.hostname;
|
||||
version = payload.version;
|
||||
plugins = payload.plugins;
|
||||
versionUpdate = payload.newVersion;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.ROUTE_TOPOLOGY: {
|
||||
routeSet = true;
|
||||
if (currentTopologyId !== payload.state.topologyId) {
|
||||
nodes = nodes.clear();
|
||||
}
|
||||
setTopology(payload.state.topologyId);
|
||||
setDefaultTopologyOptions(topologies);
|
||||
selectedNodeId = payload.state.selectedNodeId;
|
||||
pinnedMetricType = payload.state.pinnedMetricType;
|
||||
if (payload.state.controlPipe) {
|
||||
controlPipes = makeOrderedMap({
|
||||
[payload.state.controlPipe.id]:
|
||||
makeOrderedMap(payload.state.controlPipe)
|
||||
});
|
||||
} else {
|
||||
controlPipes = controlPipes.clear();
|
||||
}
|
||||
if (payload.state.nodeDetails) {
|
||||
const payloadNodeDetails = makeOrderedMap(
|
||||
payload.state.nodeDetails.map(obj => [obj.id, obj]));
|
||||
// check if detail IDs have changed
|
||||
if (!isDeepEqual(nodeDetails.keySeq(), payloadNodeDetails.keySeq())) {
|
||||
nodeDetails = payloadNodeDetails;
|
||||
}
|
||||
} else {
|
||||
nodeDetails = nodeDetails.clear();
|
||||
}
|
||||
topologyOptions = fromJS(payload.state.topologyOptions)
|
||||
|| topologyOptions;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AppStore(AppDispatcher);
|
||||
28
client/app/scripts/stores/configureStore.dev.js
Normal file
28
client/app/scripts/stores/configureStore.dev.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
// import createLogger from 'redux-logger';
|
||||
|
||||
import DevTools from '../components/dev-tools';
|
||||
import { initialState, rootReducer } from '../reducers/root';
|
||||
|
||||
export default function configureStore() {
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
initialState,
|
||||
compose(
|
||||
// applyMiddleware(thunkMiddleware, createLogger()),
|
||||
applyMiddleware(thunkMiddleware),
|
||||
DevTools.instrument()
|
||||
)
|
||||
);
|
||||
|
||||
if (module.hot) {
|
||||
// Enable Webpack hot module replacement for reducers
|
||||
module.hot.accept('../reducers/root', () => {
|
||||
const nextRootReducer = require('../reducers/root').default;
|
||||
store.replaceReducer(nextRootReducer);
|
||||
});
|
||||
}
|
||||
|
||||
return store;
|
||||
}
|
||||
5
client/app/scripts/stores/configureStore.js
Normal file
5
client/app/scripts/stores/configureStore.js
Normal file
@@ -0,0 +1,5 @@
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./configureStore.prod');
|
||||
} else {
|
||||
module.exports = require('./configureStore.dev');
|
||||
}
|
||||
12
client/app/scripts/stores/configureStore.prod.js
Normal file
12
client/app/scripts/stores/configureStore.prod.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
|
||||
import { initialState, rootReducer } from '../reducers/root';
|
||||
|
||||
export default function configureStore() {
|
||||
return createStore(
|
||||
rootReducer,
|
||||
initialState,
|
||||
applyMiddleware(thunkMiddleware)
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,19 @@
|
||||
require('../styles/main.less');
|
||||
require('../images/favicon.ico');
|
||||
|
||||
import 'babel-polyfill';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { TerminalApp } from './components/terminal-app.js';
|
||||
import configureStore from './stores/configureStore';
|
||||
import TerminalApp from './components/terminal-app.js';
|
||||
|
||||
ReactDOM.render(<TerminalApp />, document.getElementById('app'));
|
||||
const store = configureStore();
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<TerminalApp />
|
||||
</Provider>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import page from 'page';
|
||||
|
||||
import { route } from '../actions/app-actions';
|
||||
import AppStore from '../stores/app-store';
|
||||
|
||||
//
|
||||
// page.js won't match the routes below if ":state" has a slash in it, so replace those before we
|
||||
@@ -27,8 +26,24 @@ function shouldReplaceState(prevState, nextState) {
|
||||
return terminalToTerminal || closingTheTerminal;
|
||||
}
|
||||
|
||||
export function updateRoute() {
|
||||
const state = AppStore.getAppState();
|
||||
export function getUrlState(state) {
|
||||
const cp = state.get('controlPipes').last();
|
||||
const nodeDetails = state.get('nodeDetails').toIndexedSeq().map(details => ({
|
||||
id: details.id, label: details.label, topologyId: details.topologyId
|
||||
}));
|
||||
|
||||
return {
|
||||
controlPipe: cp ? cp.toJS() : null,
|
||||
nodeDetails: nodeDetails.toJS(),
|
||||
selectedNodeId: state.get('selectedNodeId'),
|
||||
pinnedMetricType: state.get('pinnedMetricType'),
|
||||
topologyId: state.get('currentTopologyId'),
|
||||
topologyOptions: state.get('topologyOptions').toJS() // all options
|
||||
};
|
||||
}
|
||||
|
||||
export function updateRoute(getState) {
|
||||
const state = getUrlState(getState());
|
||||
const stateUrl = encodeURL(JSON.stringify(state));
|
||||
const dispatch = false;
|
||||
const urlStateString = window.location.hash
|
||||
@@ -44,17 +59,19 @@ export function updateRoute() {
|
||||
}
|
||||
}
|
||||
|
||||
page('/', () => {
|
||||
updateRoute();
|
||||
});
|
||||
|
||||
page('/state/:state', (ctx) => {
|
||||
const state = JSON.parse(ctx.params.state);
|
||||
route(state);
|
||||
});
|
||||
|
||||
export function getRouter() {
|
||||
export function getRouter(dispatch, initialState) {
|
||||
// strip any trailing '/'s.
|
||||
page.base(window.location.pathname.replace(/\/$/, ''));
|
||||
|
||||
page('/', () => {
|
||||
dispatch(route(initialState));
|
||||
});
|
||||
|
||||
page('/state/:state', (ctx) => {
|
||||
const state = JSON.parse(ctx.params.state);
|
||||
dispatch(route(state));
|
||||
});
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { is as isDeepEqual, Map as makeMap, Set as makeSet } from 'immutable';
|
||||
|
||||
/**
|
||||
* Returns a cache ID based on the topologyId and optionsQuery
|
||||
@@ -66,14 +67,16 @@ export function updateTopologyIds(topologies, parentId) {
|
||||
// map for easy lookup
|
||||
export function setTopologyUrlsById(topologyUrlsById, topologies) {
|
||||
let urlMap = topologyUrlsById;
|
||||
topologies.forEach(topology => {
|
||||
urlMap = urlMap.set(topology.id, topology.url);
|
||||
if (topology.sub_topologies) {
|
||||
topology.sub_topologies.forEach(subTopology => {
|
||||
urlMap = urlMap.set(subTopology.id, subTopology.url);
|
||||
});
|
||||
}
|
||||
});
|
||||
if (topologies) {
|
||||
topologies.forEach(topology => {
|
||||
urlMap = urlMap.set(topology.id, topology.url);
|
||||
if (topology.sub_topologies) {
|
||||
topology.sub_topologies.forEach(subTopology => {
|
||||
urlMap = urlMap.set(subTopology.id, subTopology.url);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return urlMap;
|
||||
}
|
||||
|
||||
@@ -81,3 +84,56 @@ export function filterHiddenTopologies(topologies) {
|
||||
return topologies.filter(t => (!t.hide_if_empty || t.stats.node_count > 0 ||
|
||||
t.stats.filtered_nodes > 0));
|
||||
}
|
||||
|
||||
export function getActiveTopologyOptions(state) {
|
||||
// options for current topology, sub-topologies share options with parent
|
||||
const parentId = state.getIn(['currentTopology', 'parentId']);
|
||||
if (parentId) {
|
||||
return state.getIn(['topologyOptions', parentId]);
|
||||
}
|
||||
return state.getIn(['topologyOptions', state.get('currentTopologyId')]);
|
||||
}
|
||||
|
||||
export function getCurrentTopologyOptions(state) {
|
||||
return state.getIn(['currentTopology', 'options']);
|
||||
}
|
||||
|
||||
export function isTopologyEmpty(state) {
|
||||
return state.getIn(['currentTopology', 'stats', 'node_count'], 0) === 0
|
||||
&& state.get('nodes').size === 0;
|
||||
}
|
||||
|
||||
export function getAdjacentNodes(state, originNodeId) {
|
||||
let adjacentNodes = makeSet();
|
||||
const nodeId = originNodeId || state.get('selectedNodeId');
|
||||
|
||||
if (nodeId) {
|
||||
if (state.hasIn(['nodes', nodeId])) {
|
||||
adjacentNodes = makeSet(state.getIn(['nodes', nodeId, 'adjacency']));
|
||||
// fill up set with reverse edges
|
||||
state.get('nodes').forEach((node, id) => {
|
||||
if (node.get('adjacency') && node.get('adjacency').includes(nodeId)) {
|
||||
adjacentNodes = adjacentNodes.add(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return adjacentNodes;
|
||||
}
|
||||
|
||||
export function hasSelectedNode(state) {
|
||||
const selectedNodeId = state.get('selectedNodeId');
|
||||
return state.hasIn(['nodes', selectedNodeId]);
|
||||
}
|
||||
|
||||
export function getCurrentTopologyUrl(state) {
|
||||
return state.getIn(['currentTopology', 'url']);
|
||||
}
|
||||
|
||||
export function isSameTopology(nodes, nextNodes) {
|
||||
const mapper = node => makeMap({id: node.get('id'), adjacency: node.get('adjacency')});
|
||||
const topology = nodes.map(mapper);
|
||||
const nextTopology = nextNodes.map(mapper);
|
||||
return isDeepEqual(topology, nextTopology);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import debug from 'debug';
|
||||
import Immutable from 'immutable';
|
||||
|
||||
import { receiveNodesDelta } from '../actions/app-actions';
|
||||
import AppStore from '../stores/app-store';
|
||||
|
||||
const log = debug('scope:update-buffer-utils');
|
||||
const makeList = Immutable.List;
|
||||
@@ -13,8 +12,8 @@ const bufferLength = 100;
|
||||
let deltaBuffer = makeList();
|
||||
let updateTimer = null;
|
||||
|
||||
function isPaused() {
|
||||
return AppStore.isUpdatePaused();
|
||||
function isPaused(getState) {
|
||||
return getState().get('updatePausedAt') !== null;
|
||||
}
|
||||
|
||||
export function resetUpdateBuffer() {
|
||||
@@ -22,8 +21,8 @@ export function resetUpdateBuffer() {
|
||||
deltaBuffer = deltaBuffer.clear();
|
||||
}
|
||||
|
||||
function maybeUpdate() {
|
||||
if (isPaused()) {
|
||||
function maybeUpdate(getState) {
|
||||
if (isPaused(getState)) {
|
||||
clearTimeout(updateTimer);
|
||||
resetUpdateBuffer();
|
||||
} else {
|
||||
@@ -110,6 +109,6 @@ export function getUpdateBufferSize() {
|
||||
return deltaBuffer.size;
|
||||
}
|
||||
|
||||
export function resumeUpdate() {
|
||||
maybeUpdate();
|
||||
export function resumeUpdate(getState) {
|
||||
maybeUpdate(getState);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export function basePathSlash(urlPath) {
|
||||
const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const wsUrl = `${wsProto}://${location.host}${basePath(location.pathname)}`;
|
||||
|
||||
function createWebsocket(topologyUrl, optionsQuery) {
|
||||
function createWebsocket(topologyUrl, optionsQuery, dispatch) {
|
||||
if (socket) {
|
||||
socket.onclose = null;
|
||||
socket.onerror = null;
|
||||
@@ -68,67 +68,67 @@ function createWebsocket(topologyUrl, optionsQuery) {
|
||||
socket = new WebSocket(`${wsUrl}${topologyUrl}/ws?t=${updateFrequency}&${optionsQuery}`);
|
||||
|
||||
socket.onopen = () => {
|
||||
openWebsocket();
|
||||
dispatch(openWebsocket());
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
clearTimeout(reconnectTimer);
|
||||
log(`Closing websocket to ${topologyUrl}`, socket.readyState);
|
||||
socket = null;
|
||||
closeWebsocket();
|
||||
dispatch(closeWebsocket());
|
||||
|
||||
reconnectTimer = setTimeout(() => {
|
||||
createWebsocket(topologyUrl, optionsQuery);
|
||||
createWebsocket(topologyUrl, optionsQuery, dispatch);
|
||||
}, reconnectTimerInterval);
|
||||
};
|
||||
|
||||
socket.onerror = () => {
|
||||
log(`Error in websocket to ${topologyUrl}`);
|
||||
receiveError(currentUrl);
|
||||
dispatch(receiveError(currentUrl));
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
receiveNodesDelta(msg);
|
||||
dispatch(receiveNodesDelta(msg));
|
||||
};
|
||||
}
|
||||
|
||||
/* keep URLs relative */
|
||||
|
||||
export function getTopologies(options) {
|
||||
export function getTopologies(options, dispatch) {
|
||||
clearTimeout(topologyTimer);
|
||||
const optionsQuery = buildOptionsQuery(options);
|
||||
const url = `api/topology?${optionsQuery}`;
|
||||
reqwest({
|
||||
url,
|
||||
success: (res) => {
|
||||
receiveTopologies(res);
|
||||
dispatch(receiveTopologies(res));
|
||||
topologyTimer = setTimeout(() => {
|
||||
getTopologies(options);
|
||||
getTopologies(options, dispatch);
|
||||
}, TOPOLOGY_INTERVAL);
|
||||
},
|
||||
error: (err) => {
|
||||
log(`Error in topology request: ${err.responseText}`);
|
||||
receiveError(url);
|
||||
dispatch(receiveError(url));
|
||||
topologyTimer = setTimeout(() => {
|
||||
getTopologies(options);
|
||||
getTopologies(options, dispatch);
|
||||
}, TOPOLOGY_INTERVAL);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getNodesDelta(topologyUrl, options) {
|
||||
export function getNodesDelta(topologyUrl, options, dispatch) {
|
||||
const optionsQuery = buildOptionsQuery(options);
|
||||
|
||||
// only recreate websocket if url changed
|
||||
if (topologyUrl && (topologyUrl !== currentUrl || currentOptions !== optionsQuery)) {
|
||||
createWebsocket(topologyUrl, optionsQuery);
|
||||
createWebsocket(topologyUrl, optionsQuery, dispatch);
|
||||
currentUrl = topologyUrl;
|
||||
currentOptions = optionsQuery;
|
||||
}
|
||||
}
|
||||
|
||||
export function getNodeDetails(topologyUrlsById, nodeMap) {
|
||||
export function getNodeDetails(topologyUrlsById, nodeMap, dispatch) {
|
||||
// get details for all opened nodes
|
||||
const obj = nodeMap.last();
|
||||
if (obj && topologyUrlsById.has(obj.topologyId)) {
|
||||
@@ -140,42 +140,46 @@ export function getNodeDetails(topologyUrlsById, nodeMap) {
|
||||
success: (res) => {
|
||||
// make sure node is still selected
|
||||
if (nodeMap.has(res.node.id)) {
|
||||
receiveNodeDetails(res.node);
|
||||
dispatch(receiveNodeDetails(res.node));
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
log(`Error in node details request: ${err.responseText}`);
|
||||
// dont treat missing node as error
|
||||
if (err.status === 404) {
|
||||
receiveNotFound(obj.id);
|
||||
dispatch(receiveNotFound(obj.id));
|
||||
} else {
|
||||
receiveError(topologyUrl);
|
||||
dispatch(receiveError(topologyUrl));
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
} else if (obj) {
|
||||
log('No details or url found for ', obj);
|
||||
}
|
||||
}
|
||||
|
||||
export function getApiDetails() {
|
||||
export function getApiDetails(dispatch) {
|
||||
clearTimeout(apiDetailsTimer);
|
||||
const url = 'api';
|
||||
reqwest({
|
||||
url,
|
||||
success: (res) => {
|
||||
receiveApiDetails(res);
|
||||
apiDetailsTimer = setTimeout(getApiDetails, API_INTERVAL);
|
||||
dispatch(receiveApiDetails(res));
|
||||
apiDetailsTimer = setTimeout(() => {
|
||||
getApiDetails(dispatch);
|
||||
}, API_INTERVAL);
|
||||
},
|
||||
error: (err) => {
|
||||
log(`Error in api details request: ${err.responseText}`);
|
||||
receiveError(url);
|
||||
apiDetailsTimer = setTimeout(getApiDetails, API_INTERVAL / 2);
|
||||
apiDetailsTimer = setTimeout(() => {
|
||||
getApiDetails(dispatch);
|
||||
}, API_INTERVAL / 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function doControlRequest(nodeId, control) {
|
||||
export function doControlRequest(nodeId, control, dispatch) {
|
||||
clearTimeout(controlErrorTimer);
|
||||
const url = `api/control/${encodeURIComponent(control.probeId)}/`
|
||||
+ `${encodeURIComponent(control.nodeId)}/${control.id}`;
|
||||
@@ -183,26 +187,26 @@ export function doControlRequest(nodeId, control) {
|
||||
method: 'POST',
|
||||
url,
|
||||
success: (res) => {
|
||||
receiveControlSuccess(nodeId);
|
||||
dispatch(receiveControlSuccess(nodeId));
|
||||
if (res) {
|
||||
if (res.pipe) {
|
||||
receiveControlPipe(res.pipe, nodeId, res.raw_tty, true);
|
||||
dispatch(receiveControlPipe(res.pipe, nodeId, res.raw_tty, true));
|
||||
}
|
||||
if (res.removedNode) {
|
||||
receiveControlNodeRemoved(nodeId);
|
||||
dispatch(receiveControlNodeRemoved(nodeId));
|
||||
}
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
receiveControlError(nodeId, err.response);
|
||||
dispatch(receiveControlError(nodeId, err.response));
|
||||
controlErrorTimer = setTimeout(() => {
|
||||
clearControlError(nodeId);
|
||||
dispatch(clearControlError(nodeId));
|
||||
}, 10000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function deletePipe(pipeId) {
|
||||
export function deletePipe(pipeId, dispatch) {
|
||||
const url = `api/pipe/${encodeURIComponent(pipeId)}`;
|
||||
reqwest({
|
||||
method: 'DELETE',
|
||||
@@ -212,12 +216,12 @@ export function deletePipe(pipeId) {
|
||||
},
|
||||
error: (err) => {
|
||||
log(`Error closing pipe:${err}`);
|
||||
receiveError(url);
|
||||
dispatch(receiveError(url));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getPipeStatus(pipeId) {
|
||||
export function getPipeStatus(pipeId, dispatch) {
|
||||
const url = `api/pipe/${encodeURIComponent(pipeId)}/check`;
|
||||
reqwest({
|
||||
method: 'GET',
|
||||
@@ -233,7 +237,7 @@ export function getPipeStatus(pipeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
receiveControlPipeStatus(pipeId, status);
|
||||
dispatch(receiveControlPipeStatus(pipeId, status));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.1",
|
||||
"babel-polyfill": "6.7.4",
|
||||
"classnames": "~2.2.1",
|
||||
"d3": "~3.5.5",
|
||||
"dagre": "0.7.4",
|
||||
"debug": "~2.2.0",
|
||||
"filesize": "3.2.1",
|
||||
"flux": "2.1.1",
|
||||
"font-awesome": "4.5.0",
|
||||
"font-awesome-webpack": "0.0.4",
|
||||
"immutable": "~3.7.4",
|
||||
@@ -21,13 +21,17 @@
|
||||
"page": "1.7.0",
|
||||
"react": "^0.14.7",
|
||||
"react-addons-pure-render-mixin": "^0.14.7",
|
||||
"react-addons-transition-group": "^0.14.7",
|
||||
"react-addons-update": "^0.14.7",
|
||||
"react-dom": "^0.14.7",
|
||||
"react-motion": "0.3.1",
|
||||
"react-mixin": "^3.0.3",
|
||||
"react-motion": "0.3.1",
|
||||
"react-redux": "4.4.5",
|
||||
"redux": "3.5.1",
|
||||
"redux-immutable": "3.0.6",
|
||||
"redux-logger": "2.6.1",
|
||||
"redux-thunk": "2.0.1",
|
||||
"reqwest": "~2.0.5",
|
||||
"timely": "0.1.0"
|
||||
"timely": "0.1.0",
|
||||
"whatwg-fetch": "0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "6.3.3",
|
||||
@@ -55,6 +59,9 @@
|
||||
"less-loader": "2.2.2",
|
||||
"postcss-loader": "0.8.2",
|
||||
"react-addons-perf": "^0.14.0",
|
||||
"redux-devtools": "^3.2.0",
|
||||
"redux-devtools-dock-monitor": "^1.1.1",
|
||||
"redux-devtools-log-monitor": "^1.0.11",
|
||||
"style-loader": "0.13.0",
|
||||
"url": "0.11.0",
|
||||
"url-loader": "0.5.7",
|
||||
|
||||
@@ -27,8 +27,7 @@ module.exports = {
|
||||
'app': [
|
||||
'./app/scripts/main',
|
||||
'webpack-dev-server/client?http://' + WEBPACK_SERVER_HOST + ':4041',
|
||||
'webpack/hot/only-dev-server',
|
||||
'./app/scripts/debug'
|
||||
'webpack/hot/only-dev-server'
|
||||
],
|
||||
'contrast-app': [
|
||||
'./app/scripts/contrast-main',
|
||||
@@ -40,8 +39,9 @@ module.exports = {
|
||||
'webpack-dev-server/client?http://' + WEBPACK_SERVER_HOST + ':4041',
|
||||
'webpack/hot/only-dev-server'
|
||||
],
|
||||
vendors: ['classnames', 'd3', 'dagre', 'flux', 'immutable',
|
||||
'lodash', 'page', 'react', 'react-dom', 'react-motion']
|
||||
vendors: ['babel-polyfill', 'classnames', 'd3', 'dagre', 'immutable',
|
||||
'lodash', 'page', 'react', 'react-dom', 'react-redux', 'react-motion',
|
||||
'redux', 'redux-thunk']
|
||||
},
|
||||
|
||||
// This will not actually create a app.js file in ./build. It is used
|
||||
|
||||
@@ -24,8 +24,9 @@ module.exports = {
|
||||
app: './app/scripts/main',
|
||||
'contrast-app': './app/scripts/contrast-main',
|
||||
'terminal-app': './app/scripts/terminal-main',
|
||||
vendors: ['classnames', 'd3', 'dagre', 'flux', 'immutable',
|
||||
'lodash', 'page', 'react', 'react-dom', 'react-motion']
|
||||
vendors: ['babel-polyfill', 'classnames', 'd3', 'dagre', 'immutable',
|
||||
'lodash', 'page', 'react', 'react-dom', 'react-redux', 'react-motion',
|
||||
'redux', 'redux-thunk']
|
||||
},
|
||||
|
||||
output: {
|
||||
|
||||
Reference in New Issue
Block a user