Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pause Button #1106

Merged
merged 2 commits into from
Mar 7, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions client/app/scripts/actions/app-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import AppDispatcher from '../dispatcher/app-dispatcher';
import ActionTypes from '../constants/action-types';
import { saveGraph } from '../utils/file-utils';
import { updateRoute } from '../utils/router-utils';
import { bufferDeltaUpdate, resumeUpdate,
resetUpdateBuffer } from '../utils/update-buffer-utils';
import { doControlRequest, getNodesDelta, getNodeDetails,
getTopologies, deletePipe } from '../utils/web-api-utils';
import AppStore from '../stores/app-store';
Expand All @@ -19,6 +21,7 @@ export function changeTopologyOption(option, value, topologyId) {
});
updateRoute();
// update all request workers with new options
resetUpdateBuffer();
getTopologies(
AppStore.getActiveTopologyOptions()
);
Expand Down Expand Up @@ -82,6 +85,12 @@ export function clickNode(nodeId, label, origin) {
);
}

export function clickPauseUpdate() {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_PAUSE_UPDATE
});
}

export function clickRelative(nodeId, topologyId, label, origin) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_RELATIVE,
Expand All @@ -97,13 +106,21 @@ export function clickRelative(nodeId, topologyId, label, origin) {
);
}

export function clickResumeUpdate() {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_RESUME_UPDATE
});
resumeUpdate();
}

export function clickShowTopologyForNode(topologyId, nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE,
topologyId,
nodeId
});
updateRoute();
resetUpdateBuffer();
getNodesDelta(
AppStore.getCurrentTopologyUrl(),
AppStore.getActiveTopologyOptions()
Expand All @@ -116,6 +133,7 @@ export function clickTopology(topologyId) {
topologyId: topologyId
});
updateRoute();
resetUpdateBuffer();
getNodesDelta(
AppStore.getCurrentTopologyUrl(),
AppStore.getActiveTopologyOptions()
Expand Down Expand Up @@ -215,10 +233,14 @@ export function receiveNodeDetails(details) {
}

export function receiveNodesDelta(delta) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_NODES_DELTA,
delta: delta
});
if (AppStore.isUpdatePaused()) {
bufferDeltaUpdate(delta);
} else {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_NODES_DELTA,
delta: delta
});
}
}

export function receiveTopologies(topologies) {
Expand Down
39 changes: 9 additions & 30 deletions client/app/scripts/components/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import React from 'react';

import Logo from './logo';
import AppStore from '../stores/app-store';
import Footer from './footer.js';
import Sidebar from './sidebar.js';
import Status from './status.js';
import Topologies from './topologies.js';
import TopologyOptions from './topology-options.js';
import { contrastModeUrl, isContrastMode } from '../utils/contrast-utils';
import { getApiDetails, getTopologies, basePathSlash } from '../utils/web-api-utils';
import { clickDownloadGraph, clickForceRelayout, hitEsc } from '../actions/app-actions';
import { getApiDetails, getTopologies } from '../utils/web-api-utils';
import { hitEsc } from '../actions/app-actions';
import Details from './details';
import Nodes from './nodes';
import EmbeddedTerminal from './embedded-terminal';
Expand All @@ -35,6 +35,8 @@ function getStateFromStores() {
selectedNodeId: AppStore.getSelectedNodeId(),
topologies: AppStore.getTopologies(),
topologiesLoaded: AppStore.isTopologiesLoaded(),
updatePaused: AppStore.isUpdatePaused(),
updatePausedAt: AppStore.getUpdatePausedAt(),
version: AppStore.getVersion(),
websocketClosed: AppStore.isWebsocketClosed()
};
Expand Down Expand Up @@ -72,17 +74,12 @@ export default class App extends React.Component {
}

render() {
const showingDetails = this.state.nodeDetails.size > 0;
const showingTerminal = this.state.controlPipe;
const {nodeDetails, controlPipe } = this.state;
const showingDetails = nodeDetails.size > 0;
const showingTerminal = controlPipe;
// width of details panel blocking a view
const detailsWidth = showingDetails ? 450 : 0;
const topMargin = 100;
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 forceRelayoutClassName = 'footer-label footer-label-icon';
const forceRelayoutTitle = 'Force re-layout (might reduce edge crossings, but may shift nodes around)';

return (
<div className="app">
Expand Down Expand Up @@ -120,25 +117,7 @@ export default class App extends React.Component {
activeOptions={this.state.activeTopologyOptions} />
</Sidebar>

<div className="footer">
<span className="footer-label">Version</span>
{this.state.version}
<span className="footer-label">on</span>
{this.state.hostname}
&nbsp;
<a className={forceRelayoutClassName} onClick={clickForceRelayout} title={forceRelayoutTitle}>
<span className="fa fa-refresh" />
</a>
<a className="footer-label footer-label-icon" onClick={clickDownloadGraph} title="Save canvas as SVG">
<span className="fa fa-download" />
</a>
<a className="footer-label footer-label-icon" href={otherContrastModeUrl} title={otherContrastModeTitle}>
<span className="fa fa-adjust" />
</a>
<a className="footer-label footer-label-icon" href="https://gitreports.com/issue/weaveworks/scope" target="_blank" title="Report an issue">
<span className="fa fa-bug" />
</a>
</div>
<Footer {...this.state} />
</div>
);
}
Expand Down
67 changes: 67 additions & 0 deletions client/app/scripts/components/footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import moment from 'moment';

import { getUpdateBufferSize } from '../utils/update-buffer-utils';
import { contrastModeUrl, isContrastMode } from '../utils/contrast-utils';
import { clickDownloadGraph, clickForceRelayout, clickPauseUpdate,
clickResumeUpdate } from '../actions/app-actions';
import { basePathSlash } from '../utils/web-api-utils';

export default (props) => {
const { hostname, updatePaused, updatePausedAt, version } = 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)';

// 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';
}

return (
<div className="footer">

<div className="footer-status">
<span className="footer-label">Version</span>
{version}
<span className="footer-label">on</span>
{hostname}
</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={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>
</div>

</div>
);
};
2 changes: 2 additions & 0 deletions client/app/scripts/constants/action-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ const ACTION_TYPES = [
'CLICK_CLOSE_TERMINAL',
'CLICK_FORCE_RELAYOUT',
'CLICK_NODE',
'CLICK_PAUSE_UPDATE',
'CLICK_RELATIVE',
'CLICK_RESUME_UPDATE',
'CLICK_SHOW_TOPOLOGY_FOR_NODE',
'CLICK_TERMINAL',
'CLICK_TOPOLOGY',
Expand Down
27 changes: 27 additions & 0 deletions client/app/scripts/stores/app-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ let topologiesLoaded = false;
let topologyUrlsById = makeOrderedMap(); // topologyId -> topologyUrl
let routeSet = false;
let controlPipes = makeOrderedMap(); // pipeId -> controlPipe
let updatePausedAt = null; // Date
let websocketClosed = true;

// adds ID field to topology (based on last part of URL path) and save urls in
Expand Down Expand Up @@ -129,6 +130,10 @@ function closeAllNodeDetails() {
}
}

function resumeUpdate() {
updatePausedAt = null;
}

// Store API

export class AppStore extends Store {
Expand Down Expand Up @@ -263,6 +268,10 @@ export class AppStore extends Store {
return topologyUrlsById;
}

getUpdatePausedAt() {
return updatePausedAt;
}

getVersion() {
return version;
}
Expand All @@ -283,6 +292,10 @@ export class AppStore extends Store {
return currentTopology && currentTopology.stats && currentTopology.stats.node_count === 0 && nodes.size === 0;
}

isUpdatePaused() {
return updatePausedAt !== null;
}

isWebsocketClosed() {
return websocketClosed;
}
Expand All @@ -294,6 +307,7 @@ export class AppStore extends Store {

switch (payload.type) {
case ActionTypes.CHANGE_TOPOLOGY_OPTION:
resumeUpdate();
if (topologyOptions.getIn([payload.topologyId, payload.option])
!== payload.value) {
nodes = nodes.clear();
Expand Down Expand Up @@ -357,6 +371,11 @@ export class AppStore extends Store {
this.__emitChange();
break;

case ActionTypes.CLICK_PAUSE_UPDATE:
updatePausedAt = new Date;

This comment was marked as abuse.

this.__emitChange();
break;

case ActionTypes.CLICK_RELATIVE:
if (nodeDetails.has(payload.nodeId)) {
// bring to front
Expand All @@ -377,7 +396,13 @@ export class AppStore extends Store {
this.__emitChange();
break;

case ActionTypes.CLICK_RESUME_UPDATE:
resumeUpdate();
this.__emitChange();
break;

case ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE:
resumeUpdate();
nodeDetails = nodeDetails.filter((v, k) => k === payload.nodeId);
controlPipes = controlPipes.clear();
selectedNodeId = payload.nodeId;
Expand All @@ -389,6 +414,7 @@ export class AppStore extends Store {
break;

case ActionTypes.CLICK_TOPOLOGY:
resumeUpdate();
closeAllNodeDetails();
if (payload.topologyId !== currentTopologyId) {
setTopology(payload.topologyId);
Expand Down Expand Up @@ -482,6 +508,7 @@ export class AppStore extends Store {

case ActionTypes.RECEIVE_NODE_DETAILS:
errorUrl = null;

// disregard if node is not selected anymore
if (nodeDetails.has(payload.details.id)) {
nodeDetails = nodeDetails.update(payload.details.id, obj => {
Expand Down
2 changes: 2 additions & 0 deletions client/app/scripts/utils/string-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@ export function formatMetric(value, opts) {
const formatter = opts && formatters[opts.format] ? opts.format : 'number';
return formatters[formatter](value);
}

export const formatDate = d3.time.format.iso;
Loading