mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-28 01:31:17 +00:00
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:
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
|
||||
11
client/app/scripts/components/overlay.js
Normal file
11
client/app/scripts/components/overlay.js
Normal 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} />;
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user