Make API calls with time travel timestamp (#2600)

* Node details fetching reports at proper timestamp.

* Corrected all the relevant timestamps in the UI.

* Renamed some state variables.

* Time travel works for topologies list.

* Added a whole screen overlay for time travel.

* Polished the backend.

* Make time travel work also with the Resource View.

* Fixed the jest tests.

* Fixed the empty view message for resource view.

* Some naming polishing.

* Addressed the comments.
This commit is contained in:
Filip Barl
2017-06-20 12:31:22 +02:00
committed by GitHub
parent 5d6c965c47
commit eb64d3f09b
24 changed files with 292 additions and 187 deletions

View File

@@ -13,7 +13,8 @@ import Status from './status';
import Topologies from './topologies';
import TopologyOptions from './topology-options';
import CloudFeature from './cloud-feature';
import { getApiDetails, getTopologies } from '../utils/web-api-utils';
import Overlay from './overlay';
import { getApiDetails } from '../utils/web-api-utils';
import {
focusSearch,
pinNextMetric,
@@ -27,6 +28,7 @@ import {
setResourceView,
shutdown,
setViewportDimensions,
getTopologiesWithInitialPoll,
} from '../actions/app-actions';
import Details from './details';
import Nodes from './nodes';
@@ -38,7 +40,6 @@ import { getRouter, getUrlState } from '../utils/router-utils';
import { trackMixpanelEvent } from '../utils/tracking-utils';
import { availableNetworksSelector } from '../selectors/node-networks';
import {
activeTopologyOptionsSelector,
isResourceViewModeSelector,
isTableViewModeSelector,
isGraphViewModeSelector,
@@ -74,7 +75,7 @@ class App extends React.Component {
if (!this.props.routeSet || process.env.WEAVE_CLOUD) {
// dont request topologies when already done via router.
// If running as a component, always request topologies when the app mounts.
getTopologies(this.props.activeTopologyOptions, this.props.dispatch, true);
this.props.dispatch(getTopologiesWithInitialPoll());
}
getApiDetails(this.props.dispatch);
}
@@ -166,7 +167,7 @@ class App extends React.Component {
render() {
const { isTableViewMode, isGraphViewMode, isResourceViewMode, showingDetails, showingHelp,
showingNetworkSelector, showingTroubleshootingMenu } = this.props;
showingNetworkSelector, showingTroubleshootingMenu, timeTravelTransitioning } = this.props;
const isIframe = window !== window.top;
return (
@@ -203,6 +204,8 @@ class App extends React.Component {
</Sidebar>
<Footer />
<Overlay faded={timeTravelTransitioning} />
</div>
);
}
@@ -211,7 +214,6 @@ class App extends React.Component {
function mapStateToProps(state) {
return {
activeTopologyOptions: activeTopologyOptionsSelector(state),
currentTopology: state.get('currentTopology'),
isResourceViewMode: isResourceViewModeSelector(state),
isTableViewMode: isTableViewModeSelector(state),
@@ -226,6 +228,7 @@ function mapStateToProps(state) {
showingNetworkSelector: availableNetworksSelector(state).count() > 0,
showingTerminal: state.get('controlPipes').size > 0,
topologyViewMode: state.get('topologyViewMode'),
timeTravelTransitioning: state.get('timeTravelTransitioning'),
urlState: getUrlState(state)
};
}

View File

@@ -1,12 +1,14 @@
import React from 'react';
import { connect } from 'react-redux';
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';
export default class NodeDetailsInfo extends React.Component {
class NodeDetailsInfo extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
@@ -21,7 +23,7 @@ export default class NodeDetailsInfo extends React.Component {
}
render() {
const { matches = makeMap() } = this.props;
const { timestamp, matches = makeMap() } = this.props;
let rows = (this.props.rows || []);
let notShown = 0;
@@ -39,7 +41,7 @@ export default class NodeDetailsInfo extends React.Component {
return (
<div className="node-details-info">
{rows.map((field) => {
const { value, title } = formatDataType(field);
const { value, title } = formatDataType(field, timestamp);
return (
<div className="node-details-info-field" key={field.id}>
<div className="node-details-info-field-label truncate" title={field.label}>
@@ -61,3 +63,11 @@ export default class NodeDetailsInfo extends React.Component {
);
}
}
function mapStateToProps(state) {
return {
timestamp: getSerializedTimeTravelTimestamp(state),
};
}
export default connect(mapStateToProps)(NodeDetailsInfo);

View File

@@ -30,14 +30,14 @@ function getValuesForNode(node) {
return values;
}
function renderValues(node, columns = [], columnStyles = []) {
function renderValues(node, columns = [], columnStyles = [], timestamp = null) {
const fields = getValuesForNode(node);
return columns.map(({id}, i) => {
const field = fields[id];
const style = columnStyles[i];
if (field) {
if (field.valueType === 'metadata') {
const {value, title} = formatDataType(field);
const { value, title } = formatDataType(field, timestamp);
return (
<td
className="node-details-table-node-value truncate"
@@ -120,9 +120,9 @@ export default class NodeDetailsTableRow extends React.Component {
}
render() {
const { node, nodeIdKey, topologyId, columns, onClick, colStyles } = this.props;
const { node, nodeIdKey, topologyId, columns, onClick, colStyles, timestamp } = this.props;
const [firstColumnStyle, ...columnStyles] = colStyles;
const values = renderValues(node, columns, columnStyles);
const values = renderValues(node, columns, columnStyles, timestamp);
const nodeId = node[nodeIdKey];
const className = classNames('node-details-table-node', {

View File

@@ -1,15 +1,17 @@
import React from 'react';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { find, get, union, sortBy, groupBy, concat, debounce } from 'lodash';
import { NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT } from '../../constants/limits';
import { TABLE_ROW_FOCUS_DEBOUNCE_INTERVAL } from '../../constants/timer';
import ShowMore from '../show-more';
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 { TABLE_ROW_FOCUS_DEBOUNCE_INTERVAL } from '../../constants/timer';
import { getSerializedTimeTravelTimestamp } from '../../utils/web-api-utils';
import {
isIP, isNumber, defaultSortDesc, getTableColumnsStyles
} from '../../utils/node-details-utils';
@@ -125,8 +127,7 @@ function minHeightConstraint(height = 0) {
}
export default class NodeDetailsTable extends React.Component {
class NodeDetailsTable extends React.Component {
constructor(props, context) {
super(props, context);
@@ -198,7 +199,8 @@ export default class NodeDetailsTable extends React.Component {
}
render() {
const { nodeIdKey, columns, topologyId, onClickRow, onMouseEnter, onMouseLeave } = this.props;
const { nodeIdKey, columns, topologyId, onClickRow,
onMouseEnter, onMouseLeave, timestamp } = this.props;
const sortedBy = this.state.sortedBy || getDefaultSortedBy(columns, this.props.nodes);
const sortedByHeader = this.getColumnHeaders().find(h => h.id === sortedBy);
@@ -265,6 +267,7 @@ export default class NodeDetailsTable extends React.Component {
onClick={onClickRow}
onMouseEnter={this.onMouseEnterRow}
onMouseLeave={this.onMouseLeaveRow}
timestamp={timestamp}
topologyId={topologyId} />
))}
{minHeightConstraint(tableContentMinHeightConstraint)}
@@ -288,3 +291,11 @@ NodeDetailsTable.defaultProps = {
sortedDesc: null,
sortedBy: null,
};
function mapStateToProps(state) {
return {
timestamp: getSerializedTimeTravelTimestamp(state),
};
}
export default connect(mapStateToProps)(NodeDetailsTable);

View File

@@ -1,5 +1,4 @@
import React from 'react';
import classNames from 'classnames';
import { connect } from 'react-redux';
import NodesChart from '../charts/nodes-chart';
@@ -12,6 +11,7 @@ import {
isTopologyNodeCountZero,
isNodesDisplayEmpty,
} from '../utils/topology-utils';
import { nodesLoadedSelector } from '../selectors/node-filters';
import {
isGraphViewModeSelector,
isTableViewModeSelector,
@@ -53,13 +53,11 @@ class Nodes extends React.Component {
render() {
const { topologiesLoaded, nodesLoaded, topologies, currentTopology, isGraphViewMode,
isTableViewMode, isResourceViewMode, websocketTransitioning } = this.props;
const className = classNames('nodes-wrapper', { blurred: websocketTransitioning });
isTableViewMode, isResourceViewMode } = this.props;
// TODO: Rename view mode components.
return (
<div className={className}>
<div className="nodes-wrapper">
<DelayedShow delay={TOPOLOGY_LOADER_DELAY} show={!topologiesLoaded || !nodesLoaded}>
<Loading itemType="topologies" show={!topologiesLoaded} />
<Loading
@@ -85,9 +83,9 @@ function mapStateToProps(state) {
isResourceViewMode: isResourceViewModeSelector(state),
topologyNodeCountZero: isTopologyNodeCountZero(state),
nodesDisplayEmpty: isNodesDisplayEmpty(state),
websocketTransitioning: state.get('websocketTransitioning'),
nodesLoaded: nodesLoadedSelector(state),
timeTravelTransitioning: state.get('timeTravelTransitioning'),
currentTopology: state.get('currentTopology'),
nodesLoaded: state.get('nodesLoaded'),
topologies: state.get('topologies'),
topologiesLoaded: state.get('topologiesLoaded'),
};

View File

@@ -0,0 +1,11 @@
import React from 'react';
import classNames from 'classnames';
export default class Overlay extends React.Component {
render() {
const className = classNames('overlay', { faded: this.props.faded });
return <div className={className} />;
}
}

View File

@@ -1,13 +1,12 @@
import React from 'react';
import { connect } from 'react-redux';
import { isWebsocketQueryingCurrentSelector } from '../selectors/time-travel';
import { isNowSelector } from '../selectors/time-travel';
class Status extends React.Component {
render() {
const { errorUrl, topologiesLoaded, filteredNodeCount, topology,
websocketClosed, showingCurrentState } = this.props;
const { errorUrl, topologiesLoaded, filteredNodeCount, topology, websocketClosed } = this.props;
let title = '';
let text = 'Trying to reconnect...';
@@ -33,11 +32,6 @@ class Status extends React.Component {
}
classNames += ' status-stats';
showWarningIcon = false;
// TODO: Currently the stats are always pulled for the current state of the system,
// so they are incorrect when showing the past. This should be addressed somehow.
if (!showingCurrentState) {
text = '';
}
}
return (
@@ -53,7 +47,7 @@ function mapStateToProps(state) {
return {
errorUrl: state.get('errorUrl'),
filteredNodeCount: state.get('nodes').filter(node => node.get('filtered')).size,
showingCurrentState: isWebsocketQueryingCurrentSelector(state),
showingCurrentState: isNowSelector(state),
topologiesLoaded: state.get('topologiesLoaded'),
topology: state.get('currentTopology'),
websocketClosed: state.get('websocketClosed'),

View File

@@ -8,8 +8,8 @@ import { debounce } from 'lodash';
import TimeTravelTimestamp from './time-travel-timestamp';
import { trackMixpanelEvent } from '../utils/tracking-utils';
import {
websocketQueryInPast,
startWebsocketTransitionLoader,
timeTravelJumpToPast,
timeTravelStartTransition,
clickResumeUpdate,
} from '../actions/app-actions';
@@ -112,7 +112,7 @@ class TimeTravel extends React.Component {
}
this.setState({ millisecondsInPast });
this.props.startWebsocketTransitionLoader();
this.props.timeTravelStartTransition();
this.debouncedUpdateTimestamp(millisecondsInPast);
this.debouncedTrackSliderChange();
@@ -125,7 +125,7 @@ class TimeTravel extends React.Component {
if (this.state.millisecondsInPast > rangeMilliseconds) {
this.setState({ millisecondsInPast: rangeMilliseconds });
this.updateTimestamp(rangeMilliseconds);
this.props.startWebsocketTransitionLoader();
this.props.timeTravelStartTransition();
}
trackMixpanelEvent('scope.time.range.select', {
@@ -143,7 +143,7 @@ class TimeTravel extends React.Component {
rangeOptionSelected: sliderRanges.last1Hour,
});
this.updateTimestamp();
this.props.startWebsocketTransitionLoader();
this.props.timeTravelStartTransition();
trackMixpanelEvent('scope.time.now.click', {
layout: this.props.topologyViewMode,
@@ -165,7 +165,7 @@ class TimeTravel extends React.Component {
}
updateTimestamp(millisecondsInPast = 0) {
this.props.websocketQueryInPast(millisecondsInPast);
this.props.timeTravelJumpToPast(millisecondsInPast);
this.props.clickResumeUpdate();
}
@@ -215,7 +215,7 @@ class TimeTravel extends React.Component {
}
render() {
const { websocketTransitioning, hasTimeTravel } = this.props;
const { timeTravelTransitioning, hasTimeTravel } = this.props;
const { showSliderPanel, millisecondsInPast, rangeOptionSelected } = this.state;
const lowerCaseLabel = rangeOptionSelected.label.toLowerCase();
const isCurrent = (millisecondsInPast === 0);
@@ -250,7 +250,7 @@ class TimeTravel extends React.Component {
{this.renderTimeSlider()}
</div>}
<div className="time-travel-status">
{websocketTransitioning && <div className="time-travel-jump-loader">
{timeTravelTransitioning && <div className="time-travel-jump-loader">
<span className="fa fa-circle-o-notch fa-spin" />
</div>}
<TimeTravelTimestamp
@@ -270,7 +270,7 @@ function mapStateToProps({ scope, root }, { params }) {
const featureFlags = cloudInstance.featureFlags || [];
return {
hasTimeTravel: featureFlags.includes('time-travel'),
websocketTransitioning: scope.get('websocketTransitioning'),
timeTravelTransitioning: scope.get('timeTravelTransitioning'),
topologyViewMode: scope.get('topologyViewMode'),
currentTopology: scope.get('currentTopology'),
};
@@ -279,8 +279,8 @@ function mapStateToProps({ scope, root }, { params }) {
export default connect(
mapStateToProps,
{
websocketQueryInPast,
startWebsocketTransitionLoader,
timeTravelJumpToPast,
timeTravelStartTransition,
clickResumeUpdate,
}
)(TimeTravel);