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} +
+ +
+ + {pauseLabel !== '' && {pauseLabel}} + + + + + + + + + + + + + + +
+ +
+ ); +}; 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",