From 1b028e1e048b34df3874261df6a0300711079598 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Thu, 10 Aug 2017 18:02:08 +0200 Subject: [PATCH 1/3] Add fade out transition to node details panel and activate it when switching the top panel. --- client/app/scripts/actions/app-actions.js | 7 +++++++ client/app/scripts/components/node-details.js | 5 ++++- client/app/scripts/constants/action-types.js | 1 + client/app/scripts/reducers/root.js | 9 +++++++++ client/app/scripts/utils/web-api-utils.js | 4 +++- client/app/styles/_base.scss | 2 +- 6 files changed, 25 insertions(+), 3 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 88be513df..092aa1b07 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -228,6 +228,7 @@ export function clickCloseDetails(nodeId) { type: ActionTypes.CLICK_CLOSE_DETAILS, nodeId }); + getNodeDetails(getState, dispatch); updateRoute(getState); }; } @@ -545,6 +546,12 @@ export function receiveNodeDetails(details) { }; } +export function nodeDetailsStartTransition() { + return { + type: ActionTypes.NODE_DETAILS_START_TRANSITION + }; +} + export function receiveNodesDelta(delta) { return (dispatch, getState) => { if (!isPausedSelector(getState())) { diff --git a/client/app/scripts/components/node-details.js b/client/app/scripts/components/node-details.js index 6feafe0d0..c1de7cecb 100644 --- a/client/app/scripts/components/node-details.js +++ b/client/app/scripts/components/node-details.js @@ -9,6 +9,7 @@ import { brightenColor, getNeutralColor, getNodeColorDark } from '../utils/color import { isGenericTable, isPropertyList } from '../utils/node-details-utils'; import { resetDocumentTitle, setDocumentTitle } from '../utils/title-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'; @@ -151,7 +152,7 @@ class NodeDetails extends React.Component { } renderDetails() { - const { details, nodeControlStatus, nodeMatches = makeMap() } = this.props; + const { details, nodeControlStatus, transitioning, nodeMatches = makeMap() } = this.props; const showControls = details.controls && details.controls.length > 0; const nodeColor = getNodeColorDark(details.rank, details.label, details.pseudo); const {error, pending} = nodeControlStatus ? nodeControlStatus.toJS() : {}; @@ -241,6 +242,8 @@ class NodeDetails extends React.Component { /> + + ); } diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js index 5238ad391..ed0ea006d 100644 --- a/client/app/scripts/constants/action-types.js +++ b/client/app/scripts/constants/action-types.js @@ -62,6 +62,7 @@ const ACTION_TYPES = [ 'SHOW_NETWORKS', 'SHUTDOWN', 'SORT_ORDER_CHANGED', + 'NODE_DETAILS_START_TRANSITION', 'START_TIME_TRAVEL', 'TIME_TRAVEL_START_TRANSITION', 'TOGGLE_CONTRAST_MODE', diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index 9e0c9781e..53ebe3ef2 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -555,6 +555,7 @@ export function rootReducer(state = initialState, action) { state = state.updateIn(['nodeDetails', action.details.id], (obj) => { const result = Object.assign({}, obj); result.notFound = false; + result.transitioning = false; result.details = action.details; return result; }); @@ -562,6 +563,14 @@ export function rootReducer(state = initialState, action) { return state; } + case ActionTypes.NODE_DETAILS_START_TRANSITION: { + const topNode = state.get('nodeDetails').last(); + if (topNode && topNode.id) { + state = state.updateIn(['nodeDetails', topNode.id], d => ({ ...d, transitioning: true })); + } + return state; + } + case ActionTypes.SET_RECEIVED_NODES_DELTA: { // Turn on the table view if the graph is too complex, but skip // this block if the user has already loaded topologies once. diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index 328b9c412..9eb0a7844 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -7,7 +7,8 @@ import { blurSearch, clearControlError, closeWebsocket, openWebsocket, receiveEr receiveApiDetails, receiveNodesDelta, receiveNodeDetails, receiveControlError, receiveControlNodeRemoved, receiveControlPipe, receiveControlPipeStatus, receiveControlSuccess, receiveTopologies, receiveNotFound, - receiveNodesForTopology, receiveNodes } from '../actions/app-actions'; + receiveNodesForTopology, receiveNodes, nodeDetailsStartTransition +} from '../actions/app-actions'; import { getCurrentTopologyUrl } from '../utils/topology-utils'; import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; @@ -299,6 +300,7 @@ export function getNodeDetails(getState, dispatch) { } const url = urlComponents.join(''); + dispatch(nodeDetailsStartTransition()); doRequest({ url, success: (res) => { diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 63c31c36b..80a780d47 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.. From 596885ed81d0e7d77e0d62b5b13db52fb1e28091 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 11 Aug 2017 13:26:42 +0200 Subject: [PATCH 2/3] Improved the node details time transitioning logic --- client/app/scripts/actions/app-actions.js | 3 ++- client/app/scripts/components/node-details.js | 7 +++++- client/app/scripts/reducers/root.js | 25 +++++++++---------- client/app/scripts/utils/web-api-utils.js | 7 ++++-- client/app/styles/_base.scss | 4 +++ 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 092aa1b07..c6476fbf3 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -539,9 +539,10 @@ export function receiveControlSuccess(nodeId) { }; } -export function receiveNodeDetails(details) { +export function receiveNodeDetails(details, timestamp = null) { return { type: ActionTypes.RECEIVE_NODE_DETAILS, + timestamp, details }; } diff --git a/client/app/scripts/components/node-details.js b/client/app/scripts/components/node-details.js index c1de7cecb..b9c8dfaa7 100644 --- a/client/app/scripts/components/node-details.js +++ b/client/app/scripts/components/node-details.js @@ -68,7 +68,11 @@ class NodeDetails extends React.Component { onClick={this.handleShowTopologyForNode}> Show in {this.props.topologyId.replace(/-/g, ' ')} } - + ); @@ -284,6 +288,7 @@ function mapStateToProps(state, ownProps) { const currentTopologyId = state.get('currentTopologyId'); return { nodeMatches: state.getIn(['searchNodeMatches', currentTopologyId, ownProps.id]), + transitioning: state.get('pausedAt') && (!ownProps.timestamp || ownProps.timestamp.toISOString() !== state.get('pausedAt').toISOString()), 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 53ebe3ef2..afc5043dc 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -552,13 +552,12 @@ 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.transitioning = false; - result.details = action.details; - return result; - }); + console.log(action.timestamp && action.timestamp.toISOString()); + state = state.updateIn(['nodeDetails', action.details.id], obj => ({ ...obj, + notFound: false, + timestamp: action.timestamp, + details: action.details, + })); } return state; } @@ -566,7 +565,9 @@ export function rootReducer(state = initialState, action) { case ActionTypes.NODE_DETAILS_START_TRANSITION: { const topNode = state.get('nodeDetails').last(); if (topNode && topNode.id) { - state = state.updateIn(['nodeDetails', topNode.id], d => ({ ...d, transitioning: true })); + state = state.updateIn(['nodeDetails', topNode.id], obj => ({ ...obj, + transitioning: true, + })); } return state; } @@ -636,11 +637,9 @@ 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, + notFound: true, + })); } return state; } diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index 9eb0a7844..0fd00eb12 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -294,19 +294,22 @@ export function getNodeDetails(getState, dispatch) { const topologyOptions = currentTopologyId === obj.topologyId ? activeTopologyOptionsSelector(state) : makeMap(); + const timestamp = state.get('pausedAt'); const query = buildUrlQuery(topologyOptions, state); if (query) { urlComponents = urlComponents.concat(['?', query]); } const url = urlComponents.join(''); - dispatch(nodeDetailsStartTransition()); + // if (isPausedSelector(state)) { + // dispatch(nodeDetailsStartTransition()); + // } doRequest({ url, success: (res) => { // make sure node is still selected if (nodeMap.has(res.node.id)) { - dispatch(receiveNodeDetails(res.node)); + dispatch(receiveNodeDetails(res.node, timestamp)); } }, error: (err) => { diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 80a780d47..362097b08 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -737,6 +737,10 @@ top: 6px; right: 8px; + .close-details { + position: relative; + z-index: 1024; + } > span { @extend .btn-opacity; From a2de44514c2cbc350893cc7ba10e2695f7dc82e3 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 11 Aug 2017 16:26:07 +0200 Subject: [PATCH 3/3] Smarter node details transition and polishing the edge cases. --- client/app/scripts/actions/app-actions.js | 19 ++++++++-------- client/app/scripts/components/node-details.js | 8 ++++--- client/app/scripts/constants/action-types.js | 1 - client/app/scripts/reducers/root.js | 22 ++++++------------- client/app/scripts/utils/time-utils.js | 6 +++++ client/app/scripts/utils/web-api-utils.js | 14 +++++------- 6 files changed, 33 insertions(+), 37 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index c6476fbf3..960d71095 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -228,6 +228,7 @@ 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); }; @@ -539,20 +540,14 @@ export function receiveControlSuccess(nodeId) { }; } -export function receiveNodeDetails(details, timestamp = null) { +export function receiveNodeDetails(details, requestTimestamp) { return { type: ActionTypes.RECEIVE_NODE_DETAILS, - timestamp, + requestTimestamp, details }; } -export function nodeDetailsStartTransition() { - return { - type: ActionTypes.NODE_DETAILS_START_TRANSITION - }; -} - export function receiveNodesDelta(delta) { return (dispatch, getState) => { if (!isPausedSelector(getState())) { @@ -606,6 +601,9 @@ export function startTimeTravel() { if (isResourceViewModeSelector(getState())) { getResourceViewNodesSnapshot(getState(), dispatch); } + } else { + // Get most recent details before freezing the state. + getNodeDetails(getState, dispatch); } }; } @@ -739,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 b9c8dfaa7..151d627d0 100644 --- a/client/app/scripts/components/node-details.js +++ b/client/app/scripts/components/node-details.js @@ -8,6 +8,7 @@ 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'; @@ -139,6 +140,7 @@ class NodeDetails extends React.Component { Details will become available here when it communicates again.

+ ); } @@ -156,7 +158,7 @@ class NodeDetails extends React.Component { } renderDetails() { - const { details, nodeControlStatus, transitioning, nodeMatches = makeMap() } = this.props; + const { details, nodeControlStatus, nodeMatches = makeMap() } = this.props; const showControls = details.controls && details.controls.length > 0; const nodeColor = getNodeColorDark(details.rank, details.label, details.pseudo); const {error, pending} = nodeControlStatus ? nodeControlStatus.toJS() : {}; @@ -247,7 +249,7 @@ class NodeDetails extends React.Component { - + ); } @@ -287,8 +289,8 @@ 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]), - transitioning: state.get('pausedAt') && (!ownProps.timestamp || ownProps.timestamp.toISOString() !== state.get('pausedAt').toISOString()), nodes: state.get('nodes'), selectedNodeId: state.get('selectedNodeId'), }; diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js index ed0ea006d..5238ad391 100644 --- a/client/app/scripts/constants/action-types.js +++ b/client/app/scripts/constants/action-types.js @@ -62,7 +62,6 @@ const ACTION_TYPES = [ 'SHOW_NETWORKS', 'SHUTDOWN', 'SORT_ORDER_CHANGED', - 'NODE_DETAILS_START_TRANSITION', 'START_TIME_TRAVEL', 'TIME_TRAVEL_START_TRANSITION', 'TOGGLE_CONTRAST_MODE', diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index afc5043dc..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,26 +554,15 @@ export function rootReducer(state = initialState, action) { // disregard if node is not selected anymore if (state.hasIn(['nodeDetails', action.details.id])) { - console.log(action.timestamp && action.timestamp.toISOString()); state = state.updateIn(['nodeDetails', action.details.id], obj => ({ ...obj, notFound: false, - timestamp: action.timestamp, + timestamp: action.requestTimestamp, details: action.details, })); } return state; } - case ActionTypes.NODE_DETAILS_START_TRANSITION: { - const topNode = state.get('nodeDetails').last(); - if (topNode && topNode.id) { - state = state.updateIn(['nodeDetails', topNode.id], obj => ({ ...obj, - transitioning: true, - })); - } - return state; - } - case ActionTypes.SET_RECEIVED_NODES_DELTA: { // Turn on the table view if the graph is too complex, but skip // this block if the user has already loaded topologies once. @@ -638,6 +629,7 @@ export function rootReducer(state = initialState, action) { case ActionTypes.RECEIVE_NOT_FOUND: { if (state.hasIn(['nodeDetails', action.nodeId])) { state = state.updateIn(['nodeDetails', action.nodeId], obj => ({ ...obj, + timestamp: action.requestTimestamp, notFound: true, })); } 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 0fd00eb12..f6086f643 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -3,11 +3,12 @@ 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, nodeDetailsStartTransition + receiveNodesForTopology, receiveNodes, } from '../actions/app-actions'; import { getCurrentTopologyUrl } from '../utils/topology-utils'; @@ -283,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(); @@ -294,29 +296,25 @@ export function getNodeDetails(getState, dispatch) { const topologyOptions = currentTopologyId === obj.topologyId ? activeTopologyOptionsSelector(state) : makeMap(); - const timestamp = state.get('pausedAt'); const query = buildUrlQuery(topologyOptions, state); if (query) { urlComponents = urlComponents.concat(['?', query]); } const url = urlComponents.join(''); - // if (isPausedSelector(state)) { - // dispatch(nodeDetailsStartTransition()); - // } doRequest({ url, success: (res) => { // make sure node is still selected if (nodeMap.has(res.node.id)) { - dispatch(receiveNodeDetails(res.node, timestamp)); + 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)); }