mirror of
https://github.com/weaveworks/scope.git
synced 2026-08-18 03:46:45 +00:00
Merge pull request #3239 from weaveworks/3238-support-time-travel-as-prop
Support TimeTravel injection
This commit is contained in:
@@ -339,15 +339,14 @@ export function clickNode(nodeId, label, origin, topologyId = null) {
|
||||
|
||||
export function pauseTimeAtNow() {
|
||||
return (dispatch, getState) => {
|
||||
const getScopeState = () => getState().scope || getState();
|
||||
dispatch({
|
||||
type: ActionTypes.PAUSE_TIME_AT_NOW
|
||||
});
|
||||
updateRoute(getScopeState);
|
||||
if (!getScopeState().get('nodesLoaded')) {
|
||||
getNodes(getScopeState, dispatch);
|
||||
if (isResourceViewModeSelector(getScopeState())) {
|
||||
getResourceViewNodesSnapshot(getScopeState(), dispatch);
|
||||
updateRoute(getState);
|
||||
if (!getState().get('nodesLoaded')) {
|
||||
getNodes(getState, dispatch);
|
||||
if (isResourceViewModeSelector(getState())) {
|
||||
getResourceViewNodesSnapshot(getState(), dispatch);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -550,8 +549,7 @@ export function receiveNodeDetails(details, requestTimestamp) {
|
||||
|
||||
export function receiveNodesDelta(delta) {
|
||||
return (dispatch, getState) => {
|
||||
const getScopeState = () => getState().scope || getState();
|
||||
if (!isPausedSelector(getScopeState())) {
|
||||
if (!isPausedSelector(getState())) {
|
||||
// Allow css-animation to run smoothly by scheduling it to run on the
|
||||
// next tick after any potentially expensive canvas re-draws have been
|
||||
// completed.
|
||||
@@ -561,7 +559,7 @@ export function receiveNodesDelta(delta) {
|
||||
// only when the first batch of nodes delta has been received. We
|
||||
// do that because we want to keep the previous state blurred instead
|
||||
// of transitioning over an empty state like when switching topologies.
|
||||
if (getScopeState().get('timeTravelTransitioning')) {
|
||||
if (getState().get('timeTravelTransitioning')) {
|
||||
dispatch({ type: ActionTypes.FINISH_TIME_TRAVEL_TRANSITION });
|
||||
}
|
||||
|
||||
@@ -578,37 +576,17 @@ export function receiveNodesDelta(delta) {
|
||||
|
||||
export function resumeTime() {
|
||||
return (dispatch, getState) => {
|
||||
const getScopeState = () => getState().scope || getState();
|
||||
if (isPausedSelector(getScopeState())) {
|
||||
if (isPausedSelector(getState())) {
|
||||
dispatch({
|
||||
type: ActionTypes.RESUME_TIME
|
||||
});
|
||||
updateRoute(getScopeState);
|
||||
updateRoute(getState);
|
||||
// After unpausing, all of the following calls will re-activate polling.
|
||||
getTopologies(getScopeState, dispatch);
|
||||
getNodes(getScopeState, dispatch, true);
|
||||
if (isResourceViewModeSelector(getScopeState())) {
|
||||
getResourceViewNodesSnapshot(getScopeState(), dispatch);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function startTimeTravel(timestamp = null) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.START_TIME_TRAVEL,
|
||||
timestamp,
|
||||
});
|
||||
updateRoute(getState);
|
||||
if (!getState().get('nodesLoaded')) {
|
||||
getNodes(getState, dispatch);
|
||||
getTopologies(getState, dispatch);
|
||||
getNodes(getState, dispatch, true);
|
||||
if (isResourceViewModeSelector(getState())) {
|
||||
getResourceViewNodesSnapshot(getState(), dispatch);
|
||||
}
|
||||
} else {
|
||||
// Get most recent details before freezing the state.
|
||||
getNodeDetails(getState, dispatch);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -622,16 +600,20 @@ export function receiveNodes(nodes) {
|
||||
|
||||
export function jumpToTime(timestamp) {
|
||||
return (dispatch, getState) => {
|
||||
const getScopeState = () => getState().scope || getState();
|
||||
dispatch({
|
||||
type: ActionTypes.JUMP_TO_TIME,
|
||||
timestamp,
|
||||
});
|
||||
updateRoute(getScopeState);
|
||||
getNodes(getScopeState, dispatch);
|
||||
getTopologies(getScopeState, dispatch);
|
||||
if (isResourceViewModeSelector(getScopeState())) {
|
||||
getResourceViewNodesSnapshot(getScopeState(), dispatch);
|
||||
updateRoute(getState);
|
||||
getTopologies(getState, dispatch);
|
||||
if (!getState().get('nodesLoaded')) {
|
||||
getNodes(getState, dispatch);
|
||||
if (isResourceViewModeSelector(getState())) {
|
||||
getResourceViewNodesSnapshot(getState(), dispatch);
|
||||
}
|
||||
} else {
|
||||
// Get most recent details before freezing the state.
|
||||
getNodeDetails(getState, dispatch);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -646,15 +628,14 @@ export function receiveNodesForTopology(nodes, topologyId) {
|
||||
|
||||
export function receiveTopologies(topologies) {
|
||||
return (dispatch, getState) => {
|
||||
const getScopeState = () => getState().scope || getState();
|
||||
const firstLoad = !getScopeState().get('topologiesLoaded');
|
||||
const firstLoad = !getState().get('topologiesLoaded');
|
||||
dispatch({
|
||||
type: ActionTypes.RECEIVE_TOPOLOGIES,
|
||||
topologies
|
||||
});
|
||||
getNodes(getScopeState, dispatch);
|
||||
getNodes(getState, dispatch);
|
||||
// Populate search matches on first load
|
||||
const state = getScopeState();
|
||||
const state = getState();
|
||||
if (firstLoad && state.get('searchQuery')) {
|
||||
dispatch(focusSearch());
|
||||
}
|
||||
@@ -687,7 +668,7 @@ export function receiveApiDetails(apiDetails) {
|
||||
// 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));
|
||||
dispatch(jumpToTime(pausedAt));
|
||||
} else {
|
||||
dispatch(pauseTimeAtNow());
|
||||
}
|
||||
@@ -796,7 +777,7 @@ export function route(urlState) {
|
||||
if (!urlState.pausedAt) {
|
||||
dispatch(resumeTime());
|
||||
} else {
|
||||
dispatch(startTimeTravel(urlState.pausedAt));
|
||||
dispatch(jumpToTime(urlState.pausedAt));
|
||||
}
|
||||
// update all request workers with new options
|
||||
getTopologies(getState, dispatch);
|
||||
@@ -870,12 +851,6 @@ export function getImagesForService(orgId, serviceId) {
|
||||
};
|
||||
}
|
||||
|
||||
export function getFluxHistory(...params) {
|
||||
return (dispatch, getState, { actions }) => (
|
||||
dispatch(actions.getFluxHistory(...params))
|
||||
);
|
||||
}
|
||||
|
||||
export function setMonitorState(monitor) {
|
||||
return {
|
||||
type: ActionTypes.MONITOR_STATE,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import debug from 'debug';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import { debounce } from 'lodash';
|
||||
@@ -11,7 +12,6 @@ import Logo from './logo';
|
||||
import Footer from './footer';
|
||||
import Sidebar from './sidebar';
|
||||
import HelpPanel from './help-panel';
|
||||
import CloudFeature from './cloud-feature';
|
||||
import TroubleshootingMenu from './troubleshooting-menu';
|
||||
import Search from './search';
|
||||
import Status from './status';
|
||||
@@ -59,7 +59,6 @@ import {
|
||||
|
||||
const keyPressLog = debug('scope:app-key-press');
|
||||
|
||||
|
||||
class App extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
@@ -205,11 +204,7 @@ class App extends React.Component {
|
||||
{showingDetails && <Details />}
|
||||
|
||||
<div className="header">
|
||||
{timeTravelSupported && (
|
||||
<CloudFeature alwaysShow>
|
||||
<TimeTravelWrapper />
|
||||
</CloudFeature>
|
||||
)}
|
||||
{timeTravelSupported && this.props.renderTimeTravel()}
|
||||
|
||||
<div className="selectors">
|
||||
<div className="logo">
|
||||
@@ -266,8 +261,14 @@ function mapStateToProps(state) {
|
||||
};
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
renderTimeTravel: PropTypes.func,
|
||||
monitor: PropTypes.bool,
|
||||
};
|
||||
|
||||
App.defaultProps = {
|
||||
monitor: false
|
||||
renderTimeTravel: () => <TimeTravelWrapper />,
|
||||
monitor: false,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(App);
|
||||
|
||||
@@ -1,72 +1,10 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { connect } from 'react-redux';
|
||||
import { TimeTravel } from 'weaveworks-ui-components';
|
||||
import { get, orderBy, debounce } from 'lodash';
|
||||
|
||||
import { trackAnalyticsEvent } from '../utils/tracking-utils';
|
||||
import { jumpToTime, resumeTime, pauseTimeAtNow, getFluxHistory } from '../actions/app-actions';
|
||||
|
||||
// Load deployments in timeline only on zoom levels up to this range.
|
||||
const MAX_DEPLOYMENTS_RANGE_SECS = moment.duration(2, 'weeks').asSeconds();
|
||||
|
||||
// Reused from Service UI.
|
||||
const FLUX_ALL_SERVICES = '<all>';
|
||||
import { jumpToTime, resumeTime, pauseTimeAtNow } from '../actions/app-actions';
|
||||
|
||||
class TimeTravelWrapper extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isLoadingDeployments: false,
|
||||
visibleRangeStartAtSec: null,
|
||||
visibleRangeEndAtSec: null,
|
||||
};
|
||||
|
||||
this.debouncedUpdateVisibleRange = debounce(this.updateVisibleRange, 500);
|
||||
}
|
||||
|
||||
trackTimestampEdit = () => {
|
||||
trackAnalyticsEvent('scope.time.timestamp.edit', {
|
||||
layout: this.props.topologyViewMode,
|
||||
topologyId: this.props.currentTopology.get('id'),
|
||||
parentTopologyId: this.props.currentTopology.get('parentId'),
|
||||
});
|
||||
}
|
||||
|
||||
trackTimelinePanButtonClick = () => {
|
||||
trackAnalyticsEvent('scope.time.timeline.pan.button.click', {
|
||||
layout: this.props.topologyViewMode,
|
||||
topologyId: this.props.currentTopology.get('id'),
|
||||
parentTopologyId: this.props.currentTopology.get('parentId'),
|
||||
});
|
||||
}
|
||||
|
||||
trackTimelineLabelClick = () => {
|
||||
trackAnalyticsEvent('scope.time.timeline.label.click', {
|
||||
layout: this.props.topologyViewMode,
|
||||
topologyId: this.props.currentTopology.get('id'),
|
||||
parentTopologyId: this.props.currentTopology.get('parentId'),
|
||||
});
|
||||
}
|
||||
|
||||
trackTimelinePan = () => {
|
||||
trackAnalyticsEvent('scope.time.timeline.pan', {
|
||||
layout: this.props.topologyViewMode,
|
||||
topologyId: this.props.currentTopology.get('id'),
|
||||
parentTopologyId: this.props.currentTopology.get('parentId'),
|
||||
});
|
||||
}
|
||||
|
||||
trackTimelineZoom = (zoomedPeriod) => {
|
||||
trackAnalyticsEvent('scope.time.timeline.zoom', {
|
||||
layout: this.props.topologyViewMode,
|
||||
topologyId: this.props.currentTopology.get('id'),
|
||||
parentTopologyId: this.props.currentTopology.get('parentId'),
|
||||
zoomedPeriod,
|
||||
});
|
||||
}
|
||||
|
||||
handleLiveModeChange = (showingLive) => {
|
||||
if (showingLive) {
|
||||
this.props.resumeTime();
|
||||
@@ -75,96 +13,29 @@ class TimeTravelWrapper extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
updateVisibleRange = ({ startAt, endAt }) => {
|
||||
const { orgId } = this.props.params;
|
||||
|
||||
const visibleRangeEndAtSec = moment(endAt).unix();
|
||||
const visibleRangeStartAtSec = moment(startAt).unix();
|
||||
this.setState({ visibleRangeStartAtSec, visibleRangeEndAtSec });
|
||||
|
||||
// Load deployment annotations only if not zoomed out too much.
|
||||
// See https://github.com/weaveworks/service-ui/issues/1858.
|
||||
const visibleRangeSec = visibleRangeEndAtSec - visibleRangeStartAtSec;
|
||||
if (visibleRangeSec < MAX_DEPLOYMENTS_RANGE_SECS) {
|
||||
this.setState({ isLoadingDeployments: true });
|
||||
this.props
|
||||
.getFluxHistory(orgId, FLUX_ALL_SERVICES, null, endAt, true, startAt)
|
||||
.then(() => {
|
||||
this.setState({ isLoadingDeployments: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
visibleRangeStartAtSec,
|
||||
visibleRangeEndAtSec,
|
||||
} = this.state;
|
||||
|
||||
// Don't pass any deployments that are outside of the timeline visible range.
|
||||
const visibleDeployments = this.props.deployments.filter((deployment) => {
|
||||
const deploymentAtSec = moment(deployment.Stamp).unix();
|
||||
return (
|
||||
visibleRangeStartAtSec <= deploymentAtSec &&
|
||||
deploymentAtSec <= visibleRangeEndAtSec
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="tour-step-anchor time-travel-wrapper">
|
||||
<TimeTravel
|
||||
hasLiveMode
|
||||
showingLive={this.props.showingLive}
|
||||
onChangeLiveMode={this.handleLiveModeChange}
|
||||
timestamp={this.props.timestamp}
|
||||
earliestTimestamp={this.props.earliestTimestamp}
|
||||
onChangeTimestamp={this.props.jumpToTime}
|
||||
deployments={visibleDeployments}
|
||||
isLoading={this.state.isLoadingDeployments}
|
||||
onUpdateVisibleRange={this.debouncedUpdateVisibleRange}
|
||||
onTimestampInputEdit={this.trackTimestampEdit}
|
||||
onTimelinePanButtonClick={this.trackTimelinePanButtonClick}
|
||||
onTimelineLabelClick={this.trackTimelineLabelClick}
|
||||
onTimelineZoom={this.trackTimelineZoom}
|
||||
onTimelinePan={this.trackTimelinePan}
|
||||
/>
|
||||
</div>
|
||||
<TimeTravel
|
||||
hasLiveMode
|
||||
timestamp={this.props.timestamp}
|
||||
showingLive={this.props.showingLive}
|
||||
onChangeTimestamp={this.props.jumpToTime}
|
||||
onChangeLiveMode={this.handleLiveModeChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state, { params }) {
|
||||
const orgId = params && params.orgId;
|
||||
let firstSeenConnectedAt;
|
||||
|
||||
// If we're in the Weave Cloud context, use firstSeeConnectedAt as the earliest timestamp.
|
||||
if (state.root && state.root.instances) {
|
||||
const serviceInstance = state.root.instances[orgId];
|
||||
if (serviceInstance && serviceInstance.firstSeenConnectedAt) {
|
||||
firstSeenConnectedAt = moment(serviceInstance.firstSeenConnectedAt).utc().format();
|
||||
}
|
||||
}
|
||||
|
||||
const unsortedDeployments = get(
|
||||
state,
|
||||
['root', 'fluxInstanceHistory', orgId, FLUX_ALL_SERVICES],
|
||||
[]
|
||||
);
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
showingLive: !state.scope.get('pausedAt'),
|
||||
topologyViewMode: state.scope.get('topologyViewMode'),
|
||||
currentTopology: state.scope.get('currentTopology'),
|
||||
deployments: orderBy(unsortedDeployments, ['Stamp'], ['desc']),
|
||||
earliestTimestamp: firstSeenConnectedAt,
|
||||
timestamp: state.scope.get('pausedAt'),
|
||||
showingLive: !state.get('pausedAt'),
|
||||
timestamp: state.get('pausedAt'),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
jumpToTime, resumeTime, pauseTimeAtNow, getFluxHistory
|
||||
jumpToTime, resumeTime, pauseTimeAtNow
|
||||
},
|
||||
)(TimeTravelWrapper);
|
||||
|
||||
@@ -63,7 +63,6 @@ const ACTION_TYPES = [
|
||||
'SHOW_NETWORKS',
|
||||
'SHUTDOWN',
|
||||
'SORT_ORDER_CHANGED',
|
||||
'START_TIME_TRAVEL',
|
||||
'TOGGLE_CONTRAST_MODE',
|
||||
'TOGGLE_TROUBLESHOOTING_MENU',
|
||||
'UNHOVER_METRIC',
|
||||
|
||||
@@ -376,11 +376,6 @@ export function rootReducer(state = initialState, action) {
|
||||
return state.set('pausedAt', moment().utc().format());
|
||||
}
|
||||
|
||||
case ActionTypes.START_TIME_TRAVEL: {
|
||||
state = state.set('timeTravelTransitioning', false);
|
||||
return state.set('pausedAt', action.timestamp || moment().utc().format());
|
||||
}
|
||||
|
||||
case ActionTypes.JUMP_TO_TIME: {
|
||||
state = state.set('timeTravelTransitioning', true);
|
||||
return state.set('pausedAt', action.timestamp);
|
||||
|
||||
Reference in New Issue
Block a user