diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 88be513df..960d71095 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -228,6 +228,8 @@ export function clickCloseDetails(nodeId) { type: ActionTypes.CLICK_CLOSE_DETAILS, nodeId }); + // Pull the most recent details for the next details panel that comes into focus. + getNodeDetails(getState, dispatch); updateRoute(getState); }; } @@ -538,9 +540,10 @@ export function receiveControlSuccess(nodeId) { }; } -export function receiveNodeDetails(details) { +export function receiveNodeDetails(details, requestTimestamp) { return { type: ActionTypes.RECEIVE_NODE_DETAILS, + requestTimestamp, details }; } @@ -598,6 +601,9 @@ export function startTimeTravel() { if (isResourceViewModeSelector(getState())) { getResourceViewNodesSnapshot(getState(), dispatch); } + } else { + // Get most recent details before freezing the state. + getNodeDetails(getState, dispatch); } }; } @@ -731,10 +737,11 @@ export function receiveError(errorUrl) { }; } -export function receiveNotFound(nodeId) { +export function receiveNotFound(nodeId, requestTimestamp) { return { + type: ActionTypes.RECEIVE_NOT_FOUND, + requestTimestamp, nodeId, - type: ActionTypes.RECEIVE_NOT_FOUND }; } diff --git a/client/app/scripts/components/node-details.js b/client/app/scripts/components/node-details.js index 6feafe0d0..151d627d0 100644 --- a/client/app/scripts/components/node-details.js +++ b/client/app/scripts/components/node-details.js @@ -8,7 +8,9 @@ import { clickCloseDetails, clickShowTopologyForNode } from '../actions/app-acti import { brightenColor, getNeutralColor, getNodeColorDark } from '../utils/color-utils'; import { isGenericTable, isPropertyList } from '../utils/node-details-utils'; import { resetDocumentTitle, setDocumentTitle } from '../utils/title-utils'; +import { timestampsEqual } from '../utils/time-utils'; +import Overlay from './overlay'; import MatchedText from './matched-text'; import NodeDetailsControls from './node-details/node-details-controls'; import NodeDetailsGenericTable from './node-details/node-details-generic-table'; @@ -67,7 +69,11 @@ class NodeDetails extends React.Component { onClick={this.handleShowTopologyForNode}> Show in {this.props.topologyId.replace(/-/g, ' ')} } - + ); @@ -134,6 +140,7 @@ class NodeDetails extends React.Component { Details will become available here when it communicates again.

+ ); } @@ -241,6 +248,8 @@ class NodeDetails extends React.Component { /> + + ); } @@ -280,6 +289,7 @@ class NodeDetails extends React.Component { function mapStateToProps(state, ownProps) { const currentTopologyId = state.get('currentTopologyId'); return { + transitioning: !timestampsEqual(state.get('pausedAt'), ownProps.timestamp), nodeMatches: state.getIn(['searchNodeMatches', currentTopologyId, ownProps.id]), nodes: state.get('nodes'), selectedNodeId: state.get('selectedNodeId'), diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index 9e0c9781e..fccdae382 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -18,8 +18,9 @@ import { graphExceedsComplexityThreshSelector, isResourceViewModeSelector, } from '../selectors/topology'; +import { isPausedSelector } from '../selectors/time-travel'; import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/zooming'; -import { nowInSecondsPrecision } from '../utils/time-utils'; +import { nowInSecondsPrecision, timestampsEqual } from '../utils/time-utils'; import { applyPinnedSearches } from '../utils/search-utils'; import { findTopologyById, @@ -543,8 +544,9 @@ export function rootReducer(state = initialState, action) { } case ActionTypes.RECEIVE_NODE_DETAILS: { - // Freeze node details data updates after the first load when paused. - if (state.getIn(['nodeDetails', action.details.id, 'details']) && state.get('pausedAt')) { + // Ignore the update if paused and the timestamp didn't change. + const setTimestamp = state.getIn(['nodeDetails', action.details.id, 'timestamp']); + if (isPausedSelector(state) && timestampsEqual(action.requestTimestamp, setTimestamp)) { return state; } @@ -552,12 +554,11 @@ export function rootReducer(state = initialState, action) { // disregard if node is not selected anymore if (state.hasIn(['nodeDetails', action.details.id])) { - state = state.updateIn(['nodeDetails', action.details.id], (obj) => { - const result = Object.assign({}, obj); - result.notFound = false; - result.details = action.details; - return result; - }); + state = state.updateIn(['nodeDetails', action.details.id], obj => ({ ...obj, + notFound: false, + timestamp: action.requestTimestamp, + details: action.details, + })); } return state; } @@ -627,11 +628,10 @@ export function rootReducer(state = initialState, action) { case ActionTypes.RECEIVE_NOT_FOUND: { if (state.hasIn(['nodeDetails', action.nodeId])) { - state = state.updateIn(['nodeDetails', action.nodeId], (obj) => { - const result = Object.assign({}, obj); - result.notFound = true; - return result; - }); + state = state.updateIn(['nodeDetails', action.nodeId], obj => ({ ...obj, + timestamp: action.requestTimestamp, + notFound: true, + })); } return state; } diff --git a/client/app/scripts/utils/time-utils.js b/client/app/scripts/utils/time-utils.js index c93e42415..e0b576488 100644 --- a/client/app/scripts/utils/time-utils.js +++ b/client/app/scripts/utils/time-utils.js @@ -24,3 +24,9 @@ export function clampToNowInSecondsPrecision(timestamp) { export function scaleDuration(duration, scale) { return moment.duration(duration.asMilliseconds() * scale); } + +export function timestampsEqual(timestampA, timestampB) { + const stringifiedTimestampA = timestampA ? timestampA.toISOString() : ''; + const stringifiedTimestampB = timestampB ? timestampB.toISOString() : ''; + return stringifiedTimestampA === stringifiedTimestampB; +} diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index 328b9c412..f6086f643 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -3,11 +3,13 @@ import reqwest from 'reqwest'; import { defaults } from 'lodash'; import { Map as makeMap, List } from 'immutable'; -import { blurSearch, clearControlError, closeWebsocket, openWebsocket, receiveError, +import { + blurSearch, clearControlError, closeWebsocket, openWebsocket, receiveError, receiveApiDetails, receiveNodesDelta, receiveNodeDetails, receiveControlError, receiveControlNodeRemoved, receiveControlPipe, receiveControlPipeStatus, receiveControlSuccess, receiveTopologies, receiveNotFound, - receiveNodesForTopology, receiveNodes } from '../actions/app-actions'; + receiveNodesForTopology, receiveNodes, +} from '../actions/app-actions'; import { getCurrentTopologyUrl } from '../utils/topology-utils'; import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; @@ -282,6 +284,7 @@ export function getNodeDetails(getState, dispatch) { const nodeMap = state.get('nodeDetails'); const topologyUrlsById = state.get('topologyUrlsById'); const currentTopologyId = state.get('currentTopologyId'); + const requestTimestamp = state.get('pausedAt'); // get details for all opened nodes const obj = nodeMap.last(); @@ -304,14 +307,14 @@ export function getNodeDetails(getState, dispatch) { success: (res) => { // make sure node is still selected if (nodeMap.has(res.node.id)) { - dispatch(receiveNodeDetails(res.node)); + dispatch(receiveNodeDetails(res.node, requestTimestamp)); } }, error: (err) => { log(`Error in node details request: ${err.responseText}`); // dont treat missing node as error if (err.status === 404) { - dispatch(receiveNotFound(obj.id)); + dispatch(receiveNotFound(obj.id, requestTimestamp)); } else { dispatch(receiveError(topologyUrl)); } diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 63c31c36b..362097b08 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -78,7 +78,7 @@ left: 0; opacity: 0; pointer-events: none; - z-index: 2000; + z-index: 1000; &.faded { // NOTE: Not sure if we should block the pointer events here.. @@ -737,6 +737,10 @@ top: 6px; right: 8px; + .close-details { + position: relative; + z-index: 1024; + } > span { @extend .btn-opacity;