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:
David Kaltschmidt
2016-04-27 17:21:46 +02:00
parent 0f3b6bc497
commit 96aae9bc99
50 changed files with 2153 additions and 1921 deletions
@@ -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;
+44 -101
View File
@@ -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);
+56 -41
View File
@@ -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);
+25 -10
View File
@@ -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);
@@ -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);
+97 -76
View File
@@ -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);
+15 -3
View File
@@ -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);
+17 -5
View File
@@ -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);
+15 -3
View File
@@ -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);
+48 -30
View File
@@ -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);
+17 -22
View File
@@ -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);
+7 -4
View File
@@ -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);
+15 -7
View File
@@ -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);