mirror of
https://github.com/weaveworks/scope.git
synced 2026-08-18 03:46:45 +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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user