diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js
index 456a37aa1..c5bace2ee 100644
--- a/client/app/scripts/components/app.js
+++ b/client/app/scripts/components/app.js
@@ -2,19 +2,17 @@ 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 { getUpdateBufferSize } from '../utils/update-buffer-utils';
-import { contrastModeUrl, isContrastMode } from '../utils/contrast-utils';
-import { getApiDetails, getTopologies, basePathSlash } from '../utils/web-api-utils';
-import { clickDownloadGraph, clickForceRelayout, clickPauseUpdate, clickResumeUpdate, 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';
import { getRouter } from '../utils/router-utils';
-import { formatDate } from '../utils/string-utils';
import { showingDebugToolbar, DebugToolbar } from './debug-toolbar.js';
const ESC_KEY_CODE = 27;
@@ -76,25 +74,12 @@ export default class App extends React.Component {
}
render() {
- const {nodeDetails, updatePaused, updatePausedAt, controlPipe } = this.state;
+ 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)';
- const isPaused = updatePaused;
- const pauseClassName = isPaused ? 'footer-label footer-label-icon footer-label-active' : 'footer-label footer-label-icon';
- const updateCount = getUpdateBufferSize();
- const hasUpdates = updateCount > 0;
- const pauseTitle = isPaused ? `Paused on ${formatDate(updatePausedAt)}` : 'Pause updates';
- const pauseAction = isPaused ? clickResumeUpdate : clickPauseUpdate;
- const pauseWrapperClassName = isPaused ? 'footer-active' : '';
return (
@@ -132,34 +117,7 @@ export default class App extends React.Component {
activeOptions={this.state.activeTopologyOptions} />
-
-
Version
- {this.state.version}
-
on
- {this.state.hostname}
-
-
-
- {!hasUpdates && isPaused && Paused}
- {hasUpdates && isPaused && Paused ({updateCount} updates waiting)}
- {hasUpdates && !isPaused && Resuming ({updateCount} updates remaining)}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
);
}
diff --git a/client/app/scripts/components/footer.js b/client/app/scripts/components/footer.js
new file mode 100644
index 000000000..255871307
--- /dev/null
+++ b/client/app/scripts/components/footer.js
@@ -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 (
+
+
+
+ Version
+ {version}
+ on
+ {hostname}
+
+
+
+
+
+ );
+};
diff --git a/client/app/scripts/utils/update-buffer-utils.js b/client/app/scripts/utils/update-buffer-utils.js
index c40e036b1..fd96e2dfb 100644
--- a/client/app/scripts/utils/update-buffer-utils.js
+++ b/client/app/scripts/utils/update-buffer-utils.js
@@ -38,6 +38,58 @@ function maybeUpdate() {
}
}
+// consolidate first buffer entry with second
+function consolidateBuffer() {
+ const first = deltaBuffer.first();
+ deltaBuffer = deltaBuffer.shift();
+ const second = deltaBuffer.first();
+ let toAdd = _.union(first.add, second.add);
+ let toUpdate = _.union(first.update, second.update);
+ let toRemove = _.union(first.remove, second.remove);
+ log('Consolidating delta buffer', 'add', _.size(toAdd), 'update', _.size(toUpdate), 'remove', _.size(toRemove));
+
+ // check if an added node in first was updated in second -> add second update
+ toAdd = _.map(toAdd, node => {
+ const updateNode = _.find(second.update, {'id': node.id});
+ if (updateNode) {
+ toUpdate = _.reject(toUpdate, {'id': node.id});
+ return updateNode;
+ }
+ return node;
+ });
+
+ // check if an updated node in first was updated in second -> updated second update
+ // no action needed, successive updates are fine
+
+ // check if an added node in first was removed in second -> dont add, dont remove
+ _.each(first.add, node => {
+ const removedNode = _.find(second.remove, {'id': node.id});
+ if (removedNode) {
+ toAdd = _.reject(toAdd, {'id': node.id});
+ toRemove = _.reject(toRemove, {'id': node.id});
+ }
+ });
+
+ // check if an updated node in first was removed in second -> remove
+ _.each(first.update, node => {
+ const removedNode = _.find(second.remove, {'id': node.id});
+ if (removedNode) {
+ toUpdate = _.reject(toUpdate, {'id': node.id});
+ }
+ });
+
+ // check if an removed node in first was added in second -> update
+ // remove -> add is fine for the store
+
+ // update buffer
+ log('Consolidated delta buffer', 'add', _.size(toAdd), 'update', _.size(toUpdate), 'remove', _.size(toRemove));
+ deltaBuffer.set(0, {
+ add: toAdd.length > 0 ? toAdd : null,
+ update: toUpdate.length > 0 ? toUpdate : null,
+ remove: toRemove.length > 0 ? toRemove : null
+ });
+}
+
export function bufferDeltaUpdate(delta) {
if (delta.add === null && delta.update === null && delta.remove === null) {
log('Discarding empty nodes delta');
@@ -45,55 +97,7 @@ export function bufferDeltaUpdate(delta) {
}
if (deltaBuffer.size >= bufferLength) {
- // consolidate first buffer entry with second
- const first = deltaBuffer.first();
- deltaBuffer = deltaBuffer.shift();
- const second = deltaBuffer.first();
- let toAdd = _.union(first.add, second.add);
- let toUpdate = _.union(first.update, second.update);
- let toRemove = _.union(first.remove, second.remove);
- log('Consolidating delta buffer', 'add', _.size(toAdd), 'update', _.size(toUpdate), 'remove', _.size(toRemove));
-
- // check if an added node in first was updated in second -> add second update
- toAdd = _.map(toAdd, node => {
- const updateNode = _.find(second.update, {'id': node.id});
- if (updateNode) {
- toUpdate = _.reject(toUpdate, {'id': node.id});
- return updateNode;
- }
- return node;
- });
-
- // check if an updated node in first was updated in second -> updated second update
- // no action needed, successive updates are fine
-
- // check if an added node in first was removed in second -> dont add, dont remove
- _.each(first.add, node => {
- const removedNode = _.find(second.remove, {'id': node.id});
- if (removedNode) {
- toAdd = _.reject(toAdd, {'id': node.id});
- toRemove = _.reject(toRemove, {'id': node.id});
- }
- });
-
- // check if an updated node in first was removed in second -> remove
- _.each(first.update, node => {
- const removedNode = _.find(second.remove, {'id': node.id});
- if (removedNode) {
- toUpdate = _.reject(toUpdate, {'id': node.id});
- }
- });
-
- // check if an removed node in first was added in second -> update
- // remove -> add is fine for the store
-
- // update buffer
- log('Consolidated delta buffer', 'add', _.size(toAdd), 'update', _.size(toUpdate), 'remove', _.size(toRemove));
- deltaBuffer.set(0, {
- add: toAdd.length > 0 ? toAdd : null,
- update: toUpdate.length > 0 ? toUpdate : null,
- remove: toRemove.length > 0 ? toRemove : null
- });
+ consolidateBuffer();
}
deltaBuffer = deltaBuffer.push(delta);
diff --git a/client/app/styles/main.less b/client/app/styles/main.less
index 50a3e011f..50fbfef11 100644
--- a/client/app/styles/main.less
+++ b/client/app/styles/main.less
@@ -179,6 +179,7 @@ h2 {
z-index: 20;
color: @text-tertiary-color;
font-size: 0.7rem;
+ display: flex;
a {
color: @text-secondary-color;
@@ -186,33 +187,45 @@ h2 {
text-decoration: none;
font-weight: bold;
font-size: 90%;
+ cursor: pointer;
}
- &-active {
- animation: blinking 1.5s infinite ease-in-out;
+ &-status {
+ margin-right: 1em;
}
&-label {
text-transform: uppercase;
margin: 0 0.25em;
+ }
- &-icon {
- margin-left: 0.5em;
- padding: 4px 3px;
- color: @text-color;
- position: relative;
- top: -1px;
- border: 1px solid transparent;
- border-radius: 10%;
- &:hover {
- border: 1px solid @text-tertiary-color;
- }
- span {
- font-size: 150%;
- position: relative;
- top: 2px;
- }
+ &-icon {
+ margin-left: 0.5em;
+ padding: 4px 3px;
+ color: @text-color;
+ position: relative;
+ top: -1px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+
+ &:hover {
+ border: 1px solid @text-tertiary-color;
}
+
+ .fa {
+ font-size: 150%;
+ position: relative;
+ top: 2px;
+ }
+
+ &-active {
+ border: 1px solid @text-tertiary-color;
+ animation: blinking 1.5s infinite ease-in-out;
+ }
+ }
+
+ &-icon &-label {
+ margin-right: 0.5em;
}
}
diff --git a/client/package.json b/client/package.json
index 824502f96..9e2e2bf66 100644
--- a/client/package.json
+++ b/client/package.json
@@ -17,6 +17,7 @@
"immutable": "~3.7.4",
"lodash": "~3.10.1",
"materialize-css": "0.97.2",
+ "moment": "2.12.0",
"page": "1.6.4",
"react": "0.14.3",
"react-addons-pure-render-mixin": "0.14.3",