From fcb76dbfe019d57739c3a0ccd64c5e8177e4599a Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Tue, 6 Jun 2017 12:26:31 +0200 Subject: [PATCH] Addressed some of David's comments. --- client/app/scripts/actions/app-actions.js | 24 ++++--- client/app/scripts/charts/nodes-layout.js | 2 +- client/app/scripts/components/nodes.js | 10 +-- client/app/scripts/components/running-time.js | 34 --------- client/app/scripts/components/status.js | 2 +- .../scripts/components/timeline-control.js | 24 ++++--- .../components/topology-timestamp-info.js | 71 +++++++++++++++++++ .../scripts/reducers/__tests__/root-test.js | 9 +++ client/app/scripts/reducers/root.js | 10 ++- client/app/scripts/utils/web-api-utils.js | 4 +- client/app/styles/_base.scss | 36 +++++----- client/app/styles/_variables.scss | 2 - 12 files changed, 142 insertions(+), 86 deletions(-) delete mode 100644 client/app/scripts/components/running-time.js create mode 100644 client/app/scripts/components/topology-timestamp-info.js diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 2568bf9e26..e6867cc4d7 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -14,7 +14,7 @@ import { doControlRequest, getAllNodes, getResourceViewNodesSnapshot, - updateNodesDeltaChannel, + updateWebsocketChannel, getNodeDetails, getTopologies, deletePipe, @@ -214,7 +214,7 @@ export function changeTopologyOption(option, value, topologyId, addOrRemove) { resetUpdateBuffer(); const state = getState(); getTopologies(activeTopologyOptionsSelector(state), dispatch); - updateNodesDeltaChannel(state, dispatch); + updateWebsocketChannel(state, dispatch); getNodeDetails( state.get('topologyUrlsById'), state.get('currentTopologyId'), @@ -404,7 +404,7 @@ function updateTopology(dispatch, getState) { // NOTE: This is currently not needed for our static resource // view, but we'll need it here later and it's simpler to just // keep it than to redo the nodes delta updating logic. - updateNodesDeltaChannel(state, dispatch); + updateWebsocketChannel(state, dispatch); } export function clickShowTopologyForNode(topologyId, nodeId) { @@ -434,21 +434,23 @@ export function startMovingInTime() { }; } -export function websocketQueryTimestamp(timestamp) { +export function websocketQueryTimestamp(queryTimestamp) { + const requestTimestamp = moment(); // If the timestamp stands for a time less than one second ago, // assume we are actually interested in the current time. - if (timestamp && moment().diff(timestamp) >= 1000) { - timestamp = timestamp.toISOString(); + if (requestTimestamp.diff(queryTimestamp) >= 1000) { + queryTimestamp = queryTimestamp.toISOString(); } else { - timestamp = null; + queryTimestamp = null; } return (dispatch, getState) => { dispatch({ type: ActionTypes.WEBSOCKET_QUERY_TIMESTAMP, - timestamp, + requestTimestamp, + queryTimestamp, }); - updateNodesDeltaChannel(getState(), dispatch); + updateWebsocketChannel(getState(), dispatch); // update all request workers with new options resetUpdateBuffer(); }; @@ -641,7 +643,7 @@ export function receiveTopologies(topologies) { topologies }); const state = getState(); - updateNodesDeltaChannel(state, dispatch); + updateWebsocketChannel(state, dispatch); getNodeDetails( state.get('topologyUrlsById'), state.get('currentTopologyId'), @@ -758,7 +760,7 @@ export function route(urlState) { // update all request workers with new options const state = getState(); getTopologies(activeTopologyOptionsSelector(state), dispatch); - updateNodesDeltaChannel(state, dispatch); + updateWebsocketChannel(state, dispatch); getNodeDetails( state.get('topologyUrlsById'), state.get('currentTopologyId'), diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js index 6cd8bf3e53..f061f83156 100644 --- a/client/app/scripts/charts/nodes-layout.js +++ b/client/app/scripts/charts/nodes-layout.js @@ -422,7 +422,7 @@ export function doLayout(immNodes, immEdges, opts) { const cacheId = buildTopologyCacheId(options.topologyId, options.topologyOptions); // one engine and node and edge caches per topology, to keep renderings similar - if (true || options.noCache || !topologyCaches[cacheId]) { + if (options.noCache || !topologyCaches[cacheId]) { topologyCaches[cacheId] = { nodeCache: makeMap(), edgeCache: makeMap(), diff --git a/client/app/scripts/components/nodes.js b/client/app/scripts/components/nodes.js index a8989e93d8..5ad8f079f5 100644 --- a/client/app/scripts/components/nodes.js +++ b/client/app/scripts/components/nodes.js @@ -22,13 +22,15 @@ import { import { TOPOLOGY_LOADER_DELAY } from '../constants/timer'; +// TODO: The information that we already have available on the frontend should enable +// us to determine which of these cases exactly is preventing us from seeing the nodes. const NODE_COUNT_ZERO_CAUSES = [ - "We haven't received any reports from probes recently. Are the probes properly connected?", - "Containers view only: you're not running Docker, or you don't have any containers", + 'We haven\'t received any reports from probes recently. Are the probes properly connected?', + 'Containers view only: you\'re not running Docker, or you don\'t have any containers', ]; - const NODES_DISPLAY_EMPTY_CAUSES = [ - "There are nodes, but they're currently hidden. Check the view options in the bottom-left if they allow for showing hidden nodes.", + 'There are nodes, but they\'re currently hidden. Check the view options in the bottom-left if they allow for showing hidden nodes.', + 'There are no nodes for this particular moment in time. Use the timeline feature at the bottom-right corner to explore different times.', ]; const renderCauses = causes => ( diff --git a/client/app/scripts/components/running-time.js b/client/app/scripts/components/running-time.js deleted file mode 100644 index 2d4affb524..0000000000 --- a/client/app/scripts/components/running-time.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import moment from 'moment'; - - -export default class RunningTime extends React.PureComponent { - constructor(props, context) { - super(props, context); - - this.state = this.getFreshState(); - } - - componentDidMount() { - this.timer = setInterval(() => { - if (!this.props.paused) { - this.setState(this.getFreshState()); - } - }, 500); - } - - componentWillUnmount() { - clearInterval(this.timer); - } - - getFreshState() { - const timestamp = moment().utc().subtract(this.props.offsetMilliseconds); - return { humanizedTimestamp: timestamp.format('MMMM Do YYYY, h:mm:ss a') }; - } - - render() { - return ( - {this.state.humanizedTimestamp} - ); - } -} diff --git a/client/app/scripts/components/status.js b/client/app/scripts/components/status.js index 9d83bb5de8..5d99297167 100644 --- a/client/app/scripts/components/status.js +++ b/client/app/scripts/components/status.js @@ -50,7 +50,7 @@ function mapStateToProps(state) { return { errorUrl: state.get('errorUrl'), filteredNodeCount: state.get('nodes').filter(node => node.get('filtered')).size, - showingCurrentState: !state.get('websocketQueryTimestamp'), + showingCurrentState: !state.get('websocketQueryPastAt'), topologiesLoaded: state.get('topologiesLoaded'), topology: state.get('currentTopology'), websocketClosed: state.get('websocketClosed'), diff --git a/client/app/scripts/components/timeline-control.js b/client/app/scripts/components/timeline-control.js index afe46b8a61..ed478e2afa 100644 --- a/client/app/scripts/components/timeline-control.js +++ b/client/app/scripts/components/timeline-control.js @@ -5,7 +5,7 @@ import classNames from 'classnames'; import { connect } from 'react-redux'; import { debounce } from 'lodash'; -import RunningTime from './running-time'; +import TopologyTimestampInfo from './topology-timestamp-info'; import { getUpdateBufferSize } from '../utils/update-buffer-utils'; import { clickPauseUpdate, @@ -100,6 +100,13 @@ const sliderRanges = { }, }; +//
+// {this.renderRangeOption(sliderRanges.yesterday)} +// {this.renderRangeOption(sliderRanges.previousWeek)} +// {this.renderRangeOption(sliderRanges.previousMonth)} +// {this.renderRangeOption(sliderRanges.previousYear)} +//
+ class TimelineControl extends React.PureComponent { constructor(props, context) { super(props, context); @@ -199,7 +206,7 @@ class TimelineControl extends React.PureComponent { return (
{showTimelinePanel &&
- Move the slider to explore + Explore
{this.renderRangeOption(sliderRanges.last15Minutes)} @@ -219,13 +226,8 @@ class TimelineControl extends React.PureComponent { {this.renderRangeOption(sliderRanges.thisMonthSoFar)} {this.renderRangeOption(sliderRanges.thisYearSoFar)}
-
- {this.renderRangeOption(sliderRanges.yesterday)} - {this.renderRangeOption(sliderRanges.previousWeek)} - {this.renderRangeOption(sliderRanges.previousMonth)} - {this.renderRangeOption(sliderRanges.previousYear)} -
+ Move the slider to travel in time
}
- + - {pauseLabel !== '' && {pauseLabel}} + {pauseLabel !== '' && {pauseLabel}} {!showingCurrent && } diff --git a/client/app/scripts/components/topology-timestamp-info.js b/client/app/scripts/components/topology-timestamp-info.js new file mode 100644 index 0000000000..550b9a4561 --- /dev/null +++ b/client/app/scripts/components/topology-timestamp-info.js @@ -0,0 +1,71 @@ +import React from 'react'; +import moment from 'moment'; +import { connect } from 'react-redux'; + + +const TIMESTAMP_TICK_INTERVAL = 500; + +class TopologyTimestampInfo extends React.PureComponent { + constructor(props, context) { + super(props, context); + + this.state = this.getFreshState(); + } + + componentDidMount() { + this.timer = setInterval(() => { + if (!this.props.paused) { + this.setState(this.getFreshState()); + } + }, TIMESTAMP_TICK_INTERVAL); + } + + componentWillUnmount() { + clearInterval(this.timer); + } + + getFreshState() { + const { updatePausedAt, websocketQueryPastAt, websocketQueryPastRequestMadeAt } = this.props; + + let timestamp = updatePausedAt; + let showingCurrentState = false; + + if (!updatePausedAt) { + timestamp = moment().utc(); + showingCurrentState = true; + + if (websocketQueryPastAt) { + const offset = moment(websocketQueryPastRequestMadeAt).diff(moment(websocketQueryPastAt)); + timestamp = timestamp.subtract(offset); + showingCurrentState = false; + } + } + return { timestamp, showingCurrentState }; + } + + renderTimestamp() { + return ( + + ); + } + + render() { + const { showingCurrentState } = this.state; + + return ( + + {showingCurrentState ? 'now' : this.renderTimestamp()} + + ); + } +} + +function mapStateToProps(state) { + return { + updatePausedAt: state.get('updatePausedAt'), + websocketQueryPastAt: state.get('websocketQueryPastAt'), + websocketQueryPastRequestMadeAt: state.get('websocketQueryPastRequestMadeAt'), + }; +} + +export default connect(mapStateToProps)(TopologyTimestampInfo); diff --git a/client/app/scripts/reducers/__tests__/root-test.js b/client/app/scripts/reducers/__tests__/root-test.js index c408e5ec47..40062001b0 100644 --- a/client/app/scripts/reducers/__tests__/root-test.js +++ b/client/app/scripts/reducers/__tests__/root-test.js @@ -4,6 +4,7 @@ import expect from 'expect'; import { TABLE_VIEW_MODE } from '../../constants/naming'; // Root reducer test suite using Jasmine matchers import { constructEdgeId } from '../../utils/layouter-utils'; +// import { isResourceViewModeSelector } from '../../selectors/topology'; describe('RootReducer', () => { const ActionTypes = require('../../constants/action-types').default; @@ -15,6 +16,7 @@ describe('RootReducer', () => { const activeTopologyOptionsSelector = topologySelectors.activeTopologyOptionsSelector; const getAdjacentNodes = topologyUtils.getAdjacentNodes; const isTopologyEmpty = topologyUtils.isTopologyEmpty; + const isNodesDisplayEmpty = topologyUtils.isNodesDisplayEmpty; const getUrlState = require('../../utils/router-utils').getUrlState; // fixtures @@ -536,12 +538,19 @@ describe('RootReducer', () => { let nextState = initialState; nextState = reducer(nextState, ReceiveTopologiesAction); nextState = reducer(nextState, ClickTopologyAction); + expect(isTopologyEmpty(nextState)).toBeTruthy(); + + nextState = reducer(nextState, ReceiveNodesDeltaAction); expect(isTopologyEmpty(nextState)).toBeFalsy(); nextState = reducer(nextState, ClickTopology2Action); + nextState = reducer(nextState, ReceiveNodesDeltaAction); expect(isTopologyEmpty(nextState)).toBeTruthy(); nextState = reducer(nextState, ClickTopologyAction); + expect(isTopologyEmpty(nextState)).toBeTruthy(); + + nextState = reducer(nextState, ReceiveNodesDeltaAction); expect(isTopologyEmpty(nextState)).toBeFalsy(); }); diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index 869bf90623..10526ff75e 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -1,5 +1,6 @@ /* eslint-disable import/no-webpack-loader-syntax, import/no-unresolved */ import debug from 'debug'; +import moment from 'moment'; import { size, each, includes, isEqual } from 'lodash'; import { fromJS, @@ -90,7 +91,8 @@ export const initialState = makeMap({ viewport: makeMap(), websocketClosed: false, websocketMovingInTime: false, - websocketQueryTimestamp: null, + websocketQueryPastAt: null, + websocketQueryPastRequestMadeAt: null, zoomCache: makeMap(), serviceImages: makeMap() }); @@ -293,7 +295,7 @@ export function rootReducer(state = initialState, action) { } case ActionTypes.CLICK_PAUSE_UPDATE: { - return state.set('updatePausedAt', new Date()); + return state.set('updatePausedAt', moment().utc()); } case ActionTypes.CLICK_RELATIVE: { @@ -353,7 +355,9 @@ export function rootReducer(state = initialState, action) { } case ActionTypes.WEBSOCKET_QUERY_TIMESTAMP: { - return state.set('websocketQueryTimestamp', action.timestamp); + const websocketPastRequestMadeAt = action.queryTimestamp ? action.requestTimestamp : null; + state = state.set('websocketQueryPastRequestMadeAt', websocketPastRequestMadeAt); + return state.set('websocketQueryPastAt', action.queryTimestamp); } case ActionTypes.CLOSE_WEBSOCKET: { diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index 77a97a91c0..f07b752e35 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -236,10 +236,10 @@ export function getTopologies(options, dispatch, initialPoll) { }); } -export function updateNodesDeltaChannel(state, dispatch) { +export function updateWebsocketChannel(state, dispatch) { const topologyUrl = getCurrentTopologyUrl(state); const topologyOptions = activeTopologyOptionsSelector(state); - const queryTimestamp = state.get('websocketQueryTimestamp'); + const queryTimestamp = state.get('websocketQueryPastAt'); const websocketUrl = buildWebsocketUrl(topologyUrl, topologyOptions, queryTimestamp); // Only recreate websocket if url changed or if forced (weave cloud instance reload); const isNewUrl = websocketUrl !== currentUrl; diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 9296fb7529..23a810375b 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -61,6 +61,7 @@ justify-content: center; padding: 5px; position: absolute; + bottom: 11px; a { @extend .btn-opacity; @@ -68,7 +69,7 @@ border-radius: 4px; color: $text-secondary-color; cursor: pointer; - padding: 4px 3px; + padding: 1px 3px; .fa { font-size: 150%; @@ -177,7 +178,6 @@ .footer { @extend .overlay-wrapper; - bottom: 11px; right: 43px; &-status { @@ -194,6 +194,10 @@ text-transform: uppercase; } + &-tools { + display: flex; + } + &-icon { margin-left: 0.5em; } @@ -213,24 +217,22 @@ .timeline-control { @extend .overlay-wrapper; display: block; - left: calc(50vw - #{$timeline-control-width} / 2); - width: $timeline-control-width; - bottom: 0; + right: 450px; .time-status { display: flex; align-items: center; - padding: 0 9px; + justify-content: flex-end; - .running-time { font-size: 115%; } - .button { margin-left: 0.5em; } - - .jump-to-now { - @extend .blinkable; - margin-left: auto; + .topology-timestamp-info, .pause-text { + font-size: 115%; + margin-right: 5px; } - &:not(.showing-current) .running-time { + .button { margin-left: 0.5em; } + .jump-to-now { @extend .blinkable; } + + &:not(.showing-current) .topology-timestamp-info { font-weight: bold; } } @@ -259,7 +261,7 @@ } .rc-slider { - margin: 0 10px 3px; + margin: 0 10px 8px; width: auto; } } @@ -1480,9 +1482,9 @@ .sidebar { position: fixed; - bottom: 12px; - left: 12px; - padding: 4px; + bottom: 11px; + left: 11px; + padding: 5px; font-size: .7rem; border-radius: 8px; border: 1px solid transparent; diff --git a/client/app/styles/_variables.scss b/client/app/styles/_variables.scss index e60fe51316..801f8738f4 100644 --- a/client/app/styles/_variables.scss +++ b/client/app/styles/_variables.scss @@ -53,8 +53,6 @@ $link-opacity-default: 0.8; $search-border-color: transparent; $search-border-width: 1px; -$timeline-control-width: 405px; - /* specific elements */ $body-background-color: linear-gradient(30deg, $background-color 0%, $background-lighter-color 100%); $label-background-color: fade-out($background-average-color, .3);