From 4bd7fc759ab43c134abbfacafb4469d2bf1846f0 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 1 Nov 2017 18:31:26 +0100 Subject: [PATCH] Store the pausedAt state in the app URL. --- client/app/scripts/actions/app-actions.js | 46 +++++++++++++------ client/app/scripts/components/footer.js | 2 +- .../node-details/node-details-info.js | 3 +- .../node-details/node-details-table.js | 3 +- .../scripts/components/time-travel-wrapper.js | 9 ++-- client/app/scripts/reducers/root.js | 13 ++++-- client/app/scripts/utils/router-utils.js | 4 +- client/app/scripts/utils/string-utils.js | 5 +- client/app/scripts/utils/web-api-utils.js | 12 +++-- client/package.json | 2 +- client/yarn.lock | 6 +-- 11 files changed, 65 insertions(+), 40 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index ca72bba067..0c9a839162 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -345,6 +345,7 @@ export function pauseTimeAtNow() { dispatch({ type: ActionTypes.PAUSE_TIME_AT_NOW }); + updateRoute(getState); if (!getState().get('nodesLoaded')) { getNodes(getState, dispatch); if (isResourceViewModeSelector(getState())) { @@ -582,6 +583,7 @@ export function resumeTime() { dispatch({ type: ActionTypes.RESUME_TIME }); + updateRoute(getState); // After unpausing, all of the following calls will re-activate polling. getTopologies(getState, dispatch); getNodes(getState, dispatch, true); @@ -592,11 +594,13 @@ export function resumeTime() { }; } -export function startTimeTravel() { +export function startTimeTravel(timestamp = null) { return (dispatch, getState) => { dispatch({ - type: ActionTypes.START_TIME_TRAVEL + type: ActionTypes.START_TIME_TRAVEL, + timestamp, }); + updateRoute(getState); if (!getState().get('nodesLoaded')) { getNodes(getState, dispatch); if (isResourceViewModeSelector(getState())) { @@ -623,6 +627,7 @@ export function jumpToTime(timestamp) { type: ActionTypes.JUMP_TO_TIME, timestamp, }); + updateRoute(getScopeState); getNodes(getScopeState, dispatch); getTopologies(getScopeState, dispatch); if (isResourceViewModeSelector(getScopeState())) { @@ -661,13 +666,32 @@ export function receiveTopologies(topologies) { } export function receiveApiDetails(apiDetails) { - return { - type: ActionTypes.RECEIVE_API_DETAILS, - capabilities: fromJS(apiDetails.capabilities), - hostname: apiDetails.hostname, - version: apiDetails.version, - newVersion: apiDetails.newVersion, - plugins: apiDetails.plugins, + return (dispatch, getState) => { + const isFirstTime = !getState().get('version'); + const pausedAt = getState().get('pausedAt'); + + dispatch({ + type: ActionTypes.RECEIVE_API_DETAILS, + capabilities: fromJS(apiDetails.capabilities || {}), + hostname: apiDetails.hostname, + version: apiDetails.version, + newVersion: apiDetails.newVersion, + plugins: apiDetails.plugins, + }); + + // On initial load either start time travelling at the pausedAt timestamp + // (if it was given as URL param) if time travelling is enabled, otherwise + // simply pause at the present time which is arguably the next best thing + // we could do. + // NOTE: We can't make this decision before API details are received because + // we have no prior info on whether time travel would be available. + if (isFirstTime && pausedAt) { + if (apiDetails.capabilities && apiDetails.capabilities.historic_reports) { + dispatch(startTimeTravel(pausedAt)); + } else { + dispatch(pauseTimeAtNow()); + } + } }; } @@ -806,10 +830,6 @@ export function shutdown() { return (dispatch) => { stopPolling(); teardownWebsockets(); - // Exit the time travel mode before unmounting the app. - dispatch({ - type: ActionTypes.RESUME_TIME - }); dispatch({ type: ActionTypes.SHUTDOWN }); diff --git a/client/app/scripts/components/footer.js b/client/app/scripts/components/footer.js index e31d833d9d..99f7dee2ea 100644 --- a/client/app/scripts/components/footer.js +++ b/client/app/scripts/components/footer.js @@ -60,7 +60,7 @@ class Footer extends React.Component { } Version - {version} + {version || '...'} on {hostname} diff --git a/client/app/scripts/components/node-details/node-details-info.js b/client/app/scripts/components/node-details/node-details-info.js index 1434c98d95..8fc72bb0df 100644 --- a/client/app/scripts/components/node-details/node-details-info.js +++ b/client/app/scripts/components/node-details/node-details-info.js @@ -5,7 +5,6 @@ import { Map as makeMap } from 'immutable'; import MatchedText from '../matched-text'; import ShowMore from '../show-more'; import { formatDataType } from '../../utils/string-utils'; -import { getSerializedTimeTravelTimestamp } from '../../utils/web-api-utils'; class NodeDetailsInfo extends React.Component { @@ -68,7 +67,7 @@ class NodeDetailsInfo extends React.Component { function mapStateToProps(state) { return { - timestamp: getSerializedTimeTravelTimestamp(state), + timestamp: state.get('pausedAt'), }; } diff --git a/client/app/scripts/components/node-details/node-details-table.js b/client/app/scripts/components/node-details/node-details-table.js index a0a91efb03..309cfab9c2 100644 --- a/client/app/scripts/components/node-details/node-details-table.js +++ b/client/app/scripts/components/node-details/node-details-table.js @@ -11,7 +11,6 @@ import NodeDetailsTableRow from './node-details-table-row'; import NodeDetailsTableHeaders from './node-details-table-headers'; import { ipToPaddedString } from '../../utils/string-utils'; import { moveElement, insertElement } from '../../utils/array-utils'; -import { getSerializedTimeTravelTimestamp } from '../../utils/web-api-utils'; import { isIP, isNumber, defaultSortDesc, getTableColumnsStyles } from '../../utils/node-details-utils'; @@ -305,7 +304,7 @@ NodeDetailsTable.defaultProps = { function mapStateToProps(state) { return { - timestamp: getSerializedTimeTravelTimestamp(state), + timestamp: state.get('pausedAt'), }; } diff --git a/client/app/scripts/components/time-travel-wrapper.js b/client/app/scripts/components/time-travel-wrapper.js index c694ea6368..90379d60ea 100644 --- a/client/app/scripts/components/time-travel-wrapper.js +++ b/client/app/scripts/components/time-travel-wrapper.js @@ -19,7 +19,7 @@ class TimeTravelWrapper extends React.Component { } changeTimestamp(timestamp) { - this.props.jumpToTime(timestamp); + this.props.jumpToTime(moment(timestamp).utc()); } trackTimestampEdit() { @@ -61,7 +61,7 @@ class TimeTravelWrapper extends React.Component { return ( options topologyUrlsById: makeOrderedMap(), // topologyId -> topologyUrl topologyViewMode: GRAPH_VIEW_MODE, - version: '...', + version: null, versionUpdate: null, // Set some initial numerical values to prevent NaN in case of edgy race conditions. viewport: makeMap({ width: 0, height: 0 }), @@ -381,13 +381,13 @@ export function rootReducer(state = initialState, action) { case ActionTypes.PAUSE_TIME_AT_NOW: { state = state.set('showingTimeTravel', false); state = state.set('timeTravelTransitioning', false); - return state.set('pausedAt', nowInSecondsPrecision()); + return state.set('pausedAt', moment().utc()); } case ActionTypes.START_TIME_TRAVEL: { state = state.set('showingTimeTravel', true); state = state.set('timeTravelTransitioning', false); - return state.set('pausedAt', nowInSecondsPrecision()); + return state.set('pausedAt', action.timestamp || moment().utc()); } case ActionTypes.JUMP_TO_TIME: { @@ -694,6 +694,9 @@ export function rootReducer(state = initialState, action) { pinnedMetricType: action.state.pinnedMetricType }); state = state.set('topologyViewMode', action.state.topologyViewMode); + if (action.state.pausedAt) { + state = state.set('pausedAt', deserializeTimestamp(action.state.pausedAt)); + } if (action.state.gridSortedBy) { state = state.set('gridSortedBy', action.state.gridSortedBy); } diff --git a/client/app/scripts/utils/router-utils.js b/client/app/scripts/utils/router-utils.js index ce35ca4687..ae0c3a7430 100644 --- a/client/app/scripts/utils/router-utils.js +++ b/client/app/scripts/utils/router-utils.js @@ -3,6 +3,7 @@ import { each } from 'lodash'; import { route } from '../actions/app-actions'; import { storageGet, storageSet } from './storage-utils'; +import { serializeTimestamp } from './web-api-utils'; // // page.js won't match the routes below if ":state" has a slash in it, so replace those before we @@ -50,6 +51,7 @@ export function getUrlState(state) { const urlState = { controlPipe: cp ? cp.toJS() : null, nodeDetails: nodeDetails.toJS(), + pausedAt: serializeTimestamp(state.get('pausedAt')), topologyViewMode: state.get('topologyViewMode'), pinnedMetricType: state.get('pinnedMetricType'), pinnedSearches: state.get('pinnedSearches').toJS(), @@ -59,7 +61,7 @@ export function getUrlState(state) { gridSortedDesc: state.get('gridSortedDesc'), topologyId: state.get('currentTopologyId'), topologyOptions: state.get('topologyOptions').toJS(), // all options, - contrastMode: state.get('contrastMode') + contrastMode: state.get('contrastMode'), }; if (state.get('showingNetworks')) { diff --git a/client/app/scripts/utils/string-utils.js b/client/app/scripts/utils/string-utils.js index d641230552..65788be568 100644 --- a/client/app/scripts/utils/string-utils.js +++ b/client/app/scripts/utils/string-utils.js @@ -104,13 +104,12 @@ export function humanizedRoundedDownDuration(duration) { // that matches the `dataType` of the field. You must return an Object // with the keys `value` and `title` defined. // `referenceTimestamp` is the timestamp we've time-travelled to. -export function formatDataType(field, referenceTimestampStr = null) { +export function formatDataType(field, referenceTimestamp = null) { const formatters = { datetime(timestampString) { const timestamp = moment(timestampString); - const referenceTimestamp = referenceTimestampStr ? moment(referenceTimestampStr) : moment(); return { - value: timestamp.from(referenceTimestamp), + value: timestamp.from(referenceTimestamp ? moment(referenceTimestamp) : moment()), title: timestamp.utc().toISOString() }; }, diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index 6672bb847a..7f9d503b45 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -1,4 +1,5 @@ import debug from 'debug'; +import moment from 'moment'; import reqwest from 'reqwest'; import { defaults } from 'lodash'; import { Map as makeMap, List } from 'immutable'; @@ -49,16 +50,17 @@ let firstMessageOnWebsocketAt = null; let continuePolling = true; -export function getSerializedTimeTravelTimestamp(state) { - // The timestamp parameter will be used only if it's in the past. - if (!isPausedSelector(state)) return null; +export function serializeTimestamp(timestamp) { + return timestamp ? timestamp.toISOString() : null; +} - return state.get('pausedAt').toISOString(); +export function deserializeTimestamp(str) { + return str ? moment(str) : null; } export function buildUrlQuery(params = makeMap(), state) { // Attach the time travel timestamp to every request to the backend. - params = params.set('timestamp', getSerializedTimeTravelTimestamp(state)); + params = params.set('timestamp', serializeTimestamp(state.get('pausedAt'))); // Ignore the entries with values `null` or `undefined`. return params.map((value, param) => { diff --git a/client/package.json b/client/package.json index 84262ece06..0416b0f08b 100644 --- a/client/package.json +++ b/client/package.json @@ -44,7 +44,7 @@ "reselect": "3.0.1", "reselect-map": "1.0.3", "styled-components": "^2.2.1", - "weaveworks-ui-components": "git+https://github.com/weaveworks/ui-components.git#v0.1.51", + "weaveworks-ui-components": "git+https://github.com/weaveworks/ui-components.git#v0.1.52", "whatwg-fetch": "2.0.3", "xterm": "2.9.2" }, diff --git a/client/yarn.lock b/client/yarn.lock index cf7961609a..aec961cd1c 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -6666,9 +6666,9 @@ wd@^0.4.0: underscore.string "~3.0.3" vargs "~0.1.0" -"weaveworks-ui-components@git+https://github.com/weaveworks/ui-components.git#v0.1.51": - version "0.1.51" - resolved "git+https://github.com/weaveworks/ui-components.git#a08ea6a026f4cd58c66d4965838cf04d074604a4" +"weaveworks-ui-components@git+https://github.com/weaveworks/ui-components.git#v0.1.52": + version "0.1.52" + resolved "git+https://github.com/weaveworks/ui-components.git#48978545233bfb0d1ac87795f332221cdaa58fc9" dependencies: babel-cli "^6.18.0" babel-plugin-transform-export-extensions "6.8.0"