mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-18 12:59:31 +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:
@@ -415,6 +415,19 @@ type topologyStats struct {
|
||||
FilteredNodes int `json:"filtered_nodes"`
|
||||
}
|
||||
|
||||
// deserializeTimestamp converts the ISO8601 query param into a proper timestamp.
|
||||
func deserializeTimestamp(timestamp string) time.Time {
|
||||
if timestamp != "" {
|
||||
result, err := time.Parse(time.RFC3339, timestamp)
|
||||
if err != nil {
|
||||
log.Errorf("Error parsing timestamp '%s' - make sure the time format is correct", timestamp)
|
||||
}
|
||||
return result
|
||||
}
|
||||
// Default to current time if no timestamp is provided.
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// AddContainerFilters adds to the default Registry (topologyRegistry)'s containerFilters
|
||||
func AddContainerFilters(newFilters ...APITopologyOption) {
|
||||
topologyRegistry.AddContainerFilters(newFilters...)
|
||||
@@ -477,7 +490,8 @@ func (r *Registry) walk(f func(APITopologyDesc)) {
|
||||
// makeTopologyList returns a handler that yields an APITopologyList.
|
||||
func (r *Registry) makeTopologyList(rep Reporter) CtxHandlerFunc {
|
||||
return func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
report, err := rep.Report(ctx, time.Now())
|
||||
timestamp := deserializeTimestamp(req.URL.Query().Get("timestamp"))
|
||||
report, err := rep.Report(ctx, timestamp)
|
||||
if err != nil {
|
||||
respondWith(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
@@ -564,12 +578,15 @@ type rendererHandler func(context.Context, render.Renderer, render.Decorator, re
|
||||
|
||||
func (r *Registry) captureRenderer(rep Reporter, f rendererHandler) CtxHandlerFunc {
|
||||
return func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
topologyID := mux.Vars(req)["topology"]
|
||||
var (
|
||||
topologyID = mux.Vars(req)["topology"]
|
||||
timestamp = deserializeTimestamp(req.URL.Query().Get("timestamp"))
|
||||
)
|
||||
if _, ok := r.get(topologyID); !ok {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
rpt, err := rep.Report(ctx, time.Now())
|
||||
rpt, err := rep.Report(ctx, timestamp)
|
||||
if err != nil {
|
||||
respondWith(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
||||
@@ -93,20 +93,14 @@ func handleWebsocket(
|
||||
}(conn)
|
||||
|
||||
var (
|
||||
previousTopo detailed.NodeSummaries
|
||||
tick = time.Tick(loop)
|
||||
wait = make(chan struct{}, 1)
|
||||
topologyID = mux.Vars(r)["topology"]
|
||||
channelOpenedAt = time.Now()
|
||||
// By default we will always be reporting the most recent state.
|
||||
startReportingAt = time.Now()
|
||||
previousTopo detailed.NodeSummaries
|
||||
tick = time.Tick(loop)
|
||||
wait = make(chan struct{}, 1)
|
||||
topologyID = mux.Vars(r)["topology"]
|
||||
startReportingAt = deserializeTimestamp(r.Form.Get("timestamp"))
|
||||
channelOpenedAt = time.Now()
|
||||
)
|
||||
|
||||
// If the timestamp is provided explicitly by the UI, we start reporting from there.
|
||||
if timestampStr := r.Form.Get("timestamp"); timestampStr != "" {
|
||||
startReportingAt, _ = time.Parse(time.RFC3339, timestampStr)
|
||||
}
|
||||
|
||||
rep.WaitOn(ctx, wait)
|
||||
defer rep.UnWait(ctx, wait)
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
pinnedMetricSelector,
|
||||
} from '../selectors/node-metric';
|
||||
import {
|
||||
activeTopologyOptionsSelector,
|
||||
isResourceViewModeSelector,
|
||||
resourceViewAvailableSelector,
|
||||
} from '../selectors/topology';
|
||||
@@ -219,15 +218,9 @@ export function changeTopologyOption(option, value, topologyId, addOrRemove) {
|
||||
// update all request workers with new options
|
||||
dispatch(resetNodesDeltaBuffer());
|
||||
const state = getState();
|
||||
getTopologies(activeTopologyOptionsSelector(state), dispatch);
|
||||
getTopologies(state, dispatch);
|
||||
updateWebsocketChannel(state, dispatch);
|
||||
getNodeDetails(
|
||||
state.get('topologyUrlsById'),
|
||||
state.get('currentTopologyId'),
|
||||
activeTopologyOptionsSelector(state),
|
||||
state.get('nodeDetails'),
|
||||
dispatch
|
||||
);
|
||||
getNodeDetails(state, dispatch);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -336,7 +329,7 @@ export function setResourceView() {
|
||||
const firstAvailableMetricType = availableMetricTypesSelector(state).first();
|
||||
dispatch(pinMetric(firstAvailableMetricType));
|
||||
}
|
||||
getResourceViewNodesSnapshot(getState, dispatch);
|
||||
getResourceViewNodesSnapshot(getState(), dispatch);
|
||||
updateRoute(getState);
|
||||
}
|
||||
};
|
||||
@@ -351,14 +344,7 @@ export function clickNode(nodeId, label, origin) {
|
||||
nodeId
|
||||
});
|
||||
updateRoute(getState);
|
||||
const state = getState();
|
||||
getNodeDetails(
|
||||
state.get('topologyUrlsById'),
|
||||
state.get('currentTopologyId'),
|
||||
activeTopologyOptionsSelector(state),
|
||||
state.get('nodeDetails'),
|
||||
dispatch
|
||||
);
|
||||
getNodeDetails(getState(), dispatch);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -378,14 +364,7 @@ export function clickRelative(nodeId, topologyId, label, origin) {
|
||||
topologyId
|
||||
});
|
||||
updateRoute(getState);
|
||||
const state = getState();
|
||||
getNodeDetails(
|
||||
state.get('topologyUrlsById'),
|
||||
state.get('currentTopologyId'),
|
||||
activeTopologyOptionsSelector(state),
|
||||
state.get('nodeDetails'),
|
||||
dispatch
|
||||
);
|
||||
getNodeDetails(getState(), dispatch);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -393,7 +372,7 @@ function updateTopology(dispatch, getState) {
|
||||
const state = getState();
|
||||
// If we're in the resource view, get the snapshot of all the relevant node topologies.
|
||||
if (isResourceViewModeSelector(state)) {
|
||||
getResourceViewNodesSnapshot(getState, dispatch);
|
||||
getResourceViewNodesSnapshot(state, dispatch);
|
||||
}
|
||||
updateRoute(getState);
|
||||
// update all request workers with new options
|
||||
@@ -425,21 +404,25 @@ export function clickTopology(topologyId) {
|
||||
};
|
||||
}
|
||||
|
||||
export function startWebsocketTransitionLoader() {
|
||||
export function timeTravelStartTransition() {
|
||||
return {
|
||||
type: ActionTypes.START_WEBSOCKET_TRANSITION_LOADER,
|
||||
type: ActionTypes.TIME_TRAVEL_START_TRANSITION,
|
||||
};
|
||||
}
|
||||
|
||||
export function websocketQueryInPast(millisecondsInPast) {
|
||||
export function timeTravelJumpToPast(millisecondsInPast) {
|
||||
return (dispatch, getServiceState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.WEBSOCKET_QUERY_MILLISECONDS_IN_PAST,
|
||||
type: ActionTypes.TIME_TRAVEL_MILLISECONDS_IN_PAST,
|
||||
millisecondsInPast,
|
||||
});
|
||||
const scopeState = getServiceState().scope;
|
||||
updateWebsocketChannel(scopeState, dispatch);
|
||||
dispatch(resetNodesDeltaBuffer());
|
||||
getTopologies(getServiceState().scope, dispatch);
|
||||
if (isResourceViewModeSelector(scopeState)) {
|
||||
getResourceViewNodesSnapshot(scopeState, dispatch);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -504,7 +487,7 @@ export function focusSearch() {
|
||||
// the nodes delta. The solution would be to implement deeper
|
||||
// search selectors with per-node caching instead of per-topology.
|
||||
setTimeout(() => {
|
||||
getAllNodes(getState, dispatch);
|
||||
getAllNodes(getState(), dispatch);
|
||||
}, 1200);
|
||||
};
|
||||
}
|
||||
@@ -603,7 +586,7 @@ export function receiveNodesDelta(delta) {
|
||||
// returned by getState(). Since method is called from both contexts, getState()
|
||||
// will sometimes return Scope state subtree and sometimes the whole Service state.
|
||||
const state = getState().scope || getState();
|
||||
const movingInTime = state.get('websocketTransitioning');
|
||||
const movingInTime = state.get('timeTravelTransitioning');
|
||||
const hasChanges = delta.add || delta.update || delta.remove;
|
||||
|
||||
if (hasChanges || movingInTime) {
|
||||
@@ -658,7 +641,9 @@ export function receiveNodesForTopology(nodes, topologyId) {
|
||||
}
|
||||
|
||||
export function receiveTopologies(topologies) {
|
||||
return (dispatch, getState) => {
|
||||
return (dispatch, getGlobalState) => {
|
||||
// NOTE: Fortunately, this will go when Time Travel is out of <CloudFeature />.
|
||||
const getState = () => getGlobalState().scope || getGlobalState();
|
||||
const firstLoad = !getState().get('topologiesLoaded');
|
||||
dispatch({
|
||||
type: ActionTypes.RECEIVE_TOPOLOGIES,
|
||||
@@ -666,20 +651,14 @@ export function receiveTopologies(topologies) {
|
||||
});
|
||||
const state = getState();
|
||||
updateWebsocketChannel(state, dispatch);
|
||||
getNodeDetails(
|
||||
state.get('topologyUrlsById'),
|
||||
state.get('currentTopologyId'),
|
||||
activeTopologyOptionsSelector(state),
|
||||
state.get('nodeDetails'),
|
||||
dispatch
|
||||
);
|
||||
getNodeDetails(state, dispatch);
|
||||
// Populate search matches on first load
|
||||
if (firstLoad && state.get('searchQuery')) {
|
||||
dispatch(focusSearch());
|
||||
}
|
||||
// Fetch all the relevant nodes once on first load
|
||||
if (firstLoad && isResourceViewModeSelector(state)) {
|
||||
getResourceViewNodesSnapshot(getState, dispatch);
|
||||
getResourceViewNodesSnapshot(state, dispatch);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -773,6 +752,12 @@ export function setContrastMode(enabled) {
|
||||
};
|
||||
}
|
||||
|
||||
export function getTopologiesWithInitialPoll() {
|
||||
return (dispatch, getState) => {
|
||||
getTopologies(getState(), dispatch, true);
|
||||
};
|
||||
}
|
||||
|
||||
export function route(urlState) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
@@ -781,20 +766,14 @@ export function route(urlState) {
|
||||
});
|
||||
// update all request workers with new options
|
||||
const state = getState();
|
||||
getTopologies(activeTopologyOptionsSelector(state), dispatch);
|
||||
getTopologies(state, dispatch);
|
||||
updateWebsocketChannel(state, dispatch);
|
||||
getNodeDetails(
|
||||
state.get('topologyUrlsById'),
|
||||
state.get('currentTopologyId'),
|
||||
activeTopologyOptionsSelector(state),
|
||||
state.get('nodeDetails'),
|
||||
dispatch
|
||||
);
|
||||
getNodeDetails(state, dispatch);
|
||||
// If we are landing on the resource view page, we need to fetch not only all the
|
||||
// nodes for the current topology, but also the nodes of all the topologies that make
|
||||
// the layers in the resource view.
|
||||
if (isResourceViewModeSelector(state)) {
|
||||
getResourceViewNodesSnapshot(getState, dispatch);
|
||||
getResourceViewNodesSnapshot(state, dispatch);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -64,14 +64,14 @@ const ACTION_TYPES = [
|
||||
'SHOW_NETWORKS',
|
||||
'SHUTDOWN',
|
||||
'SORT_ORDER_CHANGED',
|
||||
'START_WEBSOCKET_TRANSITION_LOADER',
|
||||
'TIME_TRAVEL_MILLISECONDS_IN_PAST',
|
||||
'TIME_TRAVEL_START_TRANSITION',
|
||||
'TOGGLE_CONTRAST_MODE',
|
||||
'TOGGLE_TROUBLESHOOTING_MENU',
|
||||
'UNHOVER_METRIC',
|
||||
'UNPIN_METRIC',
|
||||
'UNPIN_NETWORK',
|
||||
'UNPIN_SEARCH',
|
||||
'WEBSOCKET_QUERY_MILLISECONDS_IN_PAST',
|
||||
];
|
||||
|
||||
export default zipObject(ACTION_TYPES, ACTION_TYPES);
|
||||
|
||||
@@ -365,7 +365,7 @@ describe('RootReducer', () => {
|
||||
|
||||
// other topology w/o options dont return options, but keep in app state
|
||||
nextState = reducer(nextState, ClickTopology2Action);
|
||||
expect(activeTopologyOptionsSelector(nextState)).toNotExist();
|
||||
expect(activeTopologyOptionsSelector(nextState).size).toEqual(0);
|
||||
expect(getUrlState(nextState).topologyOptions.topo1.option1).toEqual(['off']);
|
||||
});
|
||||
|
||||
|
||||
@@ -76,6 +76,8 @@ export const initialState = makeMap({
|
||||
showingHelp: false,
|
||||
showingTroubleshootingMenu: false,
|
||||
showingNetworks: false,
|
||||
timeTravelTransitioning: false,
|
||||
timeTravelMillisecondsInPast: 0,
|
||||
topologies: makeList(),
|
||||
topologiesLoaded: false,
|
||||
topologyOptions: makeOrderedMap(), // topologyId -> options
|
||||
@@ -86,8 +88,6 @@ export const initialState = makeMap({
|
||||
versionUpdate: null,
|
||||
viewport: makeMap(),
|
||||
websocketClosed: false,
|
||||
websocketTransitioning: false,
|
||||
websocketQueryMillisecondsInPast: 0,
|
||||
zoomCache: makeMap(),
|
||||
serviceImages: makeMap()
|
||||
});
|
||||
@@ -124,7 +124,7 @@ function processTopologies(state, nextTopologies) {
|
||||
|
||||
const topologiesWithFullnames = addTopologyFullname(topologiesWithId);
|
||||
const immNextTopologies = fromJS(topologiesWithFullnames).sortBy(topologySorter);
|
||||
return state.mergeDeepIn(['topologies'], immNextTopologies);
|
||||
return state.set('topologies', immNextTopologies);
|
||||
}
|
||||
|
||||
function setTopology(state, topologyId) {
|
||||
@@ -290,7 +290,7 @@ export function rootReducer(state = initialState, action) {
|
||||
}
|
||||
|
||||
case ActionTypes.CLICK_PAUSE_UPDATE: {
|
||||
const millisecondsInPast = state.get('websocketQueryMillisecondsInPast');
|
||||
const millisecondsInPast = state.get('timeTravelMillisecondsInPast');
|
||||
return state.set('updatePausedAt', moment().utc().subtract(millisecondsInPast));
|
||||
}
|
||||
|
||||
@@ -346,6 +346,18 @@ export function rootReducer(state = initialState, action) {
|
||||
return state;
|
||||
}
|
||||
|
||||
//
|
||||
// time travel
|
||||
//
|
||||
|
||||
case ActionTypes.TIME_TRAVEL_START_TRANSITION: {
|
||||
return state.set('timeTravelTransitioning', true);
|
||||
}
|
||||
|
||||
case ActionTypes.TIME_TRAVEL_MILLISECONDS_IN_PAST: {
|
||||
return state.set('timeTravelMillisecondsInPast', action.millisecondsInPast);
|
||||
}
|
||||
|
||||
//
|
||||
// websockets
|
||||
//
|
||||
@@ -354,14 +366,6 @@ export function rootReducer(state = initialState, action) {
|
||||
return state.set('websocketClosed', false);
|
||||
}
|
||||
|
||||
case ActionTypes.START_WEBSOCKET_TRANSITION_LOADER: {
|
||||
return state.set('websocketTransitioning', true);
|
||||
}
|
||||
|
||||
case ActionTypes.WEBSOCKET_QUERY_MILLISECONDS_IN_PAST: {
|
||||
return state.set('websocketQueryMillisecondsInPast', action.millisecondsInPast);
|
||||
}
|
||||
|
||||
case ActionTypes.CLOSE_WEBSOCKET: {
|
||||
return state.set('websocketClosed', true);
|
||||
}
|
||||
@@ -569,8 +573,8 @@ export function rootReducer(state = initialState, action) {
|
||||
// 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 (state.get('websocketTransitioning')) {
|
||||
state = state.set('websocketTransitioning', false);
|
||||
if (state.get('timeTravelTransitioning')) {
|
||||
state = state.set('timeTravelTransitioning', false);
|
||||
state = clearNodes(state);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
import { isResourceViewModeSelector } from './topology';
|
||||
import { layoutNodesByTopologyIdSelector } from './resource-view/layout';
|
||||
import { RESOURCE_VIEW_LAYERS } from '../constants/resources';
|
||||
|
||||
|
||||
export const shownNodesSelector = createSelector(
|
||||
@@ -7,3 +12,35 @@ export const shownNodesSelector = createSelector(
|
||||
],
|
||||
nodes => nodes.filter(node => !node.get('filtered'))
|
||||
);
|
||||
|
||||
export const shownResourceTopologyIdsSelector = createSelector(
|
||||
[
|
||||
layoutNodesByTopologyIdSelector,
|
||||
],
|
||||
layoutNodesByTopologyId => layoutNodesByTopologyId.filter(nodes => !nodes.isEmpty()).keySeq()
|
||||
);
|
||||
|
||||
// TODO: Get rid of this logic by unifying `nodes` and `nodesByTopology` global states.
|
||||
const loadedAllResourceLayersSelector = createSelector(
|
||||
[
|
||||
state => state.get('nodesByTopology').keySeq(),
|
||||
],
|
||||
resourceViewLoadedTopologyIds => fromJS(RESOURCE_VIEW_LAYERS).keySeq()
|
||||
.every(topId => resourceViewLoadedTopologyIds.contains(topId))
|
||||
);
|
||||
|
||||
export const nodesLoadedSelector = createSelector(
|
||||
[
|
||||
state => state.get('nodesLoaded'),
|
||||
loadedAllResourceLayersSelector,
|
||||
isResourceViewModeSelector,
|
||||
],
|
||||
(nodesLoaded, loadedAllResourceLayers, isResourceViewMode) => (
|
||||
// Since `nodesLoaded` is set when we receive nodes delta over websockets,
|
||||
// it's a completely wrong criterion for determining if resource view is
|
||||
// in the loading state - instead we look at the 'static' topologies whose
|
||||
// nodes were loaded into 'nodesByTopology' and say resource view has been
|
||||
// loaded if nodes for all the resource layer topologies have been loaded once.
|
||||
isResourceViewMode ? loadedAllResourceLayers : nodesLoaded
|
||||
)
|
||||
);
|
||||
|
||||
@@ -8,9 +8,10 @@ export const isPausedSelector = createSelector(
|
||||
updatePausedAt => updatePausedAt !== null
|
||||
);
|
||||
|
||||
export const isWebsocketQueryingCurrentSelector = createSelector(
|
||||
export const isNowSelector = createSelector(
|
||||
[
|
||||
state => state.get('websocketQueryMillisecondsInPast')
|
||||
state => state.get('timeTravelMillisecondsInPast')
|
||||
],
|
||||
websocketQueryMillisecondsInPast => websocketQueryMillisecondsInPast === 0
|
||||
// true for values 0, undefined, null, etc...
|
||||
timeTravelMillisecondsInPast => !(timeTravelMillisecondsInPast > 0)
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { Map as makeMap } from 'immutable';
|
||||
|
||||
import { layersTopologyIdsSelector } from './resource-view/layout';
|
||||
import {
|
||||
@@ -56,6 +57,6 @@ export const activeTopologyOptionsSelector = createSelector(
|
||||
state => state.get('topologyOptions'),
|
||||
],
|
||||
(parentTopologyId, currentTopologyId, topologyOptions) => (
|
||||
topologyOptions.get(parentTopologyId || currentTopologyId)
|
||||
topologyOptions.get(parentTopologyId || currentTopologyId, makeMap())
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import MockDate from 'mockdate';
|
||||
import { Map as makeMap, OrderedMap as makeOrderedMap } from 'immutable';
|
||||
|
||||
import {OrderedMap as makeOrderedMap} from 'immutable';
|
||||
import { buildUrlQuery, basePath, getApiPath, getWebsocketUrl } from '../web-api-utils';
|
||||
|
||||
|
||||
describe('WebApiUtils', () => {
|
||||
describe('basePath', () => {
|
||||
it('should handle /scope/terminal.html', () => {
|
||||
@@ -22,15 +24,34 @@ describe('WebApiUtils', () => {
|
||||
});
|
||||
|
||||
describe('buildUrlQuery', () => {
|
||||
let state = makeMap();
|
||||
|
||||
beforeEach(() => {
|
||||
MockDate.set(1434319925275);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
MockDate.reset();
|
||||
});
|
||||
|
||||
it('should handle empty options', () => {
|
||||
expect(buildUrlQuery(makeOrderedMap({}))).toBe('');
|
||||
expect(buildUrlQuery(makeOrderedMap([]), state)).toBe('');
|
||||
});
|
||||
|
||||
it('should combine multiple options', () => {
|
||||
state = state.set('timeTravelMillisecondsInPast', 0);
|
||||
expect(buildUrlQuery(makeOrderedMap([
|
||||
['foo', 2],
|
||||
['bar', 4]
|
||||
]))).toBe('foo=2&bar=4');
|
||||
]), state)).toBe('foo=2&bar=4');
|
||||
});
|
||||
|
||||
it('should combine multiple options with a timestamp', () => {
|
||||
state = state.set('timeTravelMillisecondsInPast', 60 * 60 * 1000); // 1h in the past
|
||||
expect(buildUrlQuery(makeOrderedMap([
|
||||
['foo', 2],
|
||||
['bar', 4]
|
||||
]), state)).toBe('foo=2&bar=4×tamp=2015-06-14T21:12:05.275Z');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -89,18 +89,20 @@ export function ipToPaddedString(value) {
|
||||
// Formats metadata values. Add a key to the `formatters` obj
|
||||
// that matches the `dataType` of the field. You must return an Object
|
||||
// with the keys `value` and `title` defined.
|
||||
export function formatDataType(field) {
|
||||
// `referenceTimestamp` is the timestamp we've time-travelled to.
|
||||
export function formatDataType(field, referenceTimestampStr = null) {
|
||||
const formatters = {
|
||||
datetime(dateString) {
|
||||
const date = moment(new Date(dateString));
|
||||
datetime(timestampString) {
|
||||
const timestamp = moment(timestampString);
|
||||
const referenceTimestamp = referenceTimestampStr ? moment(referenceTimestampStr) : moment();
|
||||
return {
|
||||
value: date.fromNow(),
|
||||
title: date.format('YYYY-MM-DD HH:mm:ss.SSS')
|
||||
value: timestamp.from(referenceTimestamp),
|
||||
title: timestamp.utc().toISOString()
|
||||
};
|
||||
}
|
||||
};
|
||||
const format = formatters[field.dataType];
|
||||
return format
|
||||
? format(field.value)
|
||||
: {value: field.value, title: field.value};
|
||||
: { value: field.value, title: field.value };
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { endsWith } from 'lodash';
|
||||
import { Set as makeSet, List as makeList } from 'immutable';
|
||||
|
||||
import { isWebsocketQueryingCurrentSelector } from '../selectors/time-travel';
|
||||
import { isNowSelector } from '../selectors/time-travel';
|
||||
import { isResourceViewModeSelector } from '../selectors/topology';
|
||||
import { pinnedMetricSelector } from '../selectors/node-metric';
|
||||
import { shownNodesSelector } from '../selectors/node-filters';
|
||||
import { shownNodesSelector, shownResourceTopologyIdsSelector } from '../selectors/node-filters';
|
||||
|
||||
//
|
||||
// top priority first
|
||||
@@ -137,13 +137,15 @@ export function getCurrentTopologyOptions(state) {
|
||||
|
||||
export function isTopologyNodeCountZero(state) {
|
||||
const nodeCount = state.getIn(['currentTopology', 'stats', 'node_count'], 0);
|
||||
return nodeCount === 0 && isWebsocketQueryingCurrentSelector(state);
|
||||
// If we are browsing the past, assume there would normally be some nodes at different times.
|
||||
// If we are in the resource view, don't rely on these stats at all (for now).
|
||||
return nodeCount === 0 && isNowSelector(state) && !isResourceViewModeSelector(state);
|
||||
}
|
||||
|
||||
export function isNodesDisplayEmpty(state) {
|
||||
// Consider a topology in the resource view empty if it has no pinned metric.
|
||||
if (isResourceViewModeSelector(state)) {
|
||||
return !pinnedMetricSelector(state);
|
||||
return !pinnedMetricSelector(state) || shownResourceTopologyIdsSelector(state).isEmpty();
|
||||
}
|
||||
// Otherwise (in graph and table view), we only look at the nodes content.
|
||||
return shownNodesSelector(state).isEmpty();
|
||||
|
||||
@@ -2,7 +2,7 @@ import debug from 'debug';
|
||||
import moment from 'moment';
|
||||
import reqwest from 'reqwest';
|
||||
import { defaults } from 'lodash';
|
||||
import { fromJS, Map as makeMap, List } from 'immutable';
|
||||
import { Map as makeMap, List } from 'immutable';
|
||||
|
||||
import { blurSearch, clearControlError, closeWebsocket, openWebsocket, receiveError,
|
||||
receiveApiDetails, receiveNodesDelta, receiveNodeDetails, receiveControlError,
|
||||
@@ -13,7 +13,8 @@ import { blurSearch, clearControlError, closeWebsocket, openWebsocket, receiveEr
|
||||
import { getCurrentTopologyUrl } from '../utils/topology-utils';
|
||||
import { layersTopologyIdsSelector } from '../selectors/resource-view/layout';
|
||||
import { activeTopologyOptionsSelector } from '../selectors/topology';
|
||||
import { isWebsocketQueryingCurrentSelector } from '../selectors/time-travel';
|
||||
import { isNowSelector } from '../selectors/time-travel';
|
||||
|
||||
import { API_REFRESH_INTERVAL, TOPOLOGY_REFRESH_INTERVAL } from '../constants/timer';
|
||||
|
||||
const log = debug('scope:web-api-utils');
|
||||
@@ -46,8 +47,18 @@ let createWebsocketAt = null;
|
||||
let firstMessageOnWebsocketAt = null;
|
||||
let continuePolling = true;
|
||||
|
||||
export function buildUrlQuery(params) {
|
||||
if (!params) return '';
|
||||
|
||||
export function getSerializedTimeTravelTimestamp(state) {
|
||||
// The timestamp parameter will be used only if it's in the past.
|
||||
if (isNowSelector(state)) return null;
|
||||
|
||||
const millisecondsInPast = state.get('timeTravelMillisecondsInPast');
|
||||
return moment().utc().subtract(millisecondsInPast).toISOString();
|
||||
}
|
||||
|
||||
export function buildUrlQuery(params = makeMap(), state) {
|
||||
// Attach the time travel timestamp to every request to the backend.
|
||||
params = params.set('timestamp', getSerializedTimeTravelTimestamp(state));
|
||||
|
||||
// Ignore the entries with values `null` or `undefined`.
|
||||
return params.map((value, param) => {
|
||||
@@ -97,13 +108,10 @@ export function getWebsocketUrl(host = window.location.host, pathname = window.l
|
||||
return `${wsProto}://${host}${process.env.SCOPE_API_PREFIX || ''}${basePath(pathname)}`;
|
||||
}
|
||||
|
||||
function buildWebsocketUrl(topologyUrl, topologyOptions = makeMap(), queryTimestamp) {
|
||||
const query = buildUrlQuery(fromJS({
|
||||
t: updateFrequency,
|
||||
timestamp: queryTimestamp,
|
||||
...topologyOptions.toJS(),
|
||||
}));
|
||||
return `${getWebsocketUrl()}${topologyUrl}/ws?${query}`;
|
||||
function buildWebsocketUrl(topologyUrl, topologyOptions = makeMap(), state) {
|
||||
topologyOptions = topologyOptions.set('t', updateFrequency);
|
||||
const optionsQuery = buildUrlQuery(topologyOptions, state);
|
||||
return `${getWebsocketUrl()}${topologyUrl}/ws?${optionsQuery}`;
|
||||
}
|
||||
|
||||
function createWebsocket(websocketUrl, dispatch) {
|
||||
@@ -179,12 +187,12 @@ function doRequest(opts) {
|
||||
/**
|
||||
* Does a one-time fetch of all the nodes for a custom list of topologies.
|
||||
*/
|
||||
function getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions = makeMap()) {
|
||||
function getNodesForTopologies(state, dispatch, topologyIds, topologyOptions = makeMap()) {
|
||||
// fetch sequentially
|
||||
getState().get('topologyUrlsById')
|
||||
state.get('topologyUrlsById')
|
||||
.filter((_, topologyId) => topologyIds.contains(topologyId))
|
||||
.reduce((sequence, topologyUrl, topologyId) => sequence.then(() => {
|
||||
const optionsQuery = buildUrlQuery(topologyOptions.get(topologyId));
|
||||
const optionsQuery = buildUrlQuery(topologyOptions.get(topologyId), state);
|
||||
return doRequest({ url: `${getApiPath()}${topologyUrl}?${optionsQuery}` });
|
||||
})
|
||||
.then(json => dispatch(receiveNodesForTopology(json.nodes, topologyId))),
|
||||
@@ -194,27 +202,28 @@ function getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions
|
||||
/**
|
||||
* Gets nodes for all topologies (for search).
|
||||
*/
|
||||
export function getAllNodes(getState, dispatch) {
|
||||
const state = getState();
|
||||
export function getAllNodes(state, dispatch) {
|
||||
const topologyOptions = state.get('topologyOptions');
|
||||
const topologyIds = state.get('topologyUrlsById').keySeq();
|
||||
getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions);
|
||||
getNodesForTopologies(state, dispatch, topologyIds, topologyOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* One-time update of all the nodes of topologies that appear in the current resource view.
|
||||
* TODO: Replace the one-time snapshot with periodic polling.
|
||||
*/
|
||||
export function getResourceViewNodesSnapshot(getState, dispatch) {
|
||||
const topologyIds = layersTopologyIdsSelector(getState());
|
||||
getNodesForTopologies(getState, dispatch, topologyIds);
|
||||
export function getResourceViewNodesSnapshot(state, dispatch) {
|
||||
const topologyIds = layersTopologyIdsSelector(state);
|
||||
getNodesForTopologies(state, dispatch, topologyIds);
|
||||
}
|
||||
|
||||
export function getTopologies(options, dispatch, initialPoll) {
|
||||
export function getTopologies(state, dispatch, initialPoll = false) {
|
||||
// TODO: Remove this once TimeTravel is out of the feature flag.
|
||||
state = state.scope || state;
|
||||
// Used to resume polling when navigating between pages in Weave Cloud.
|
||||
continuePolling = initialPoll === true ? true : continuePolling;
|
||||
clearTimeout(topologyTimer);
|
||||
const optionsQuery = buildUrlQuery(options);
|
||||
const optionsQuery = buildUrlQuery(activeTopologyOptionsSelector(state), state);
|
||||
const url = `${getApiPath()}/api/topology?${optionsQuery}`;
|
||||
doRequest({
|
||||
url,
|
||||
@@ -222,7 +231,7 @@ export function getTopologies(options, dispatch, initialPoll) {
|
||||
if (continuePolling) {
|
||||
dispatch(receiveTopologies(res));
|
||||
topologyTimer = setTimeout(() => {
|
||||
getTopologies(options, dispatch);
|
||||
getTopologies(state, dispatch);
|
||||
}, TOPOLOGY_REFRESH_INTERVAL);
|
||||
}
|
||||
},
|
||||
@@ -232,26 +241,17 @@ export function getTopologies(options, dispatch, initialPoll) {
|
||||
// Only retry in stand-alone mode
|
||||
if (continuePolling) {
|
||||
topologyTimer = setTimeout(() => {
|
||||
getTopologies(options, dispatch);
|
||||
getTopologies(state, dispatch);
|
||||
}, TOPOLOGY_REFRESH_INTERVAL);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getWebsocketQueryTimestamp(state) {
|
||||
// The timestamp query parameter will be used only if it's in the past.
|
||||
if (isWebsocketQueryingCurrentSelector(state)) return null;
|
||||
|
||||
const millisecondsInPast = state.get('websocketQueryMillisecondsInPast');
|
||||
return moment().utc().subtract(millisecondsInPast).toISOString();
|
||||
}
|
||||
|
||||
export function updateWebsocketChannel(state, dispatch) {
|
||||
const topologyUrl = getCurrentTopologyUrl(state);
|
||||
const topologyOptions = activeTopologyOptionsSelector(state);
|
||||
const queryTimestamp = getWebsocketQueryTimestamp(state);
|
||||
const websocketUrl = buildWebsocketUrl(topologyUrl, topologyOptions, queryTimestamp);
|
||||
const websocketUrl = buildWebsocketUrl(topologyUrl, topologyOptions, state);
|
||||
// Only recreate websocket if url changed or if forced (weave cloud instance reload);
|
||||
const isNewUrl = websocketUrl !== currentUrl;
|
||||
// `topologyUrl` can be undefined initially, so only create a socket if it is truthy
|
||||
@@ -262,7 +262,11 @@ export function updateWebsocketChannel(state, dispatch) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getNodeDetails(topologyUrlsById, currentTopologyId, options, nodeMap, dispatch) {
|
||||
export function getNodeDetails(state, dispatch) {
|
||||
const nodeMap = state.get('nodeDetails');
|
||||
const topologyUrlsById = state.get('topologyUrlsById');
|
||||
const currentTopologyId = state.get('currentTopologyId');
|
||||
|
||||
// get details for all opened nodes
|
||||
const obj = nodeMap.last();
|
||||
if (obj && topologyUrlsById.has(obj.topologyId)) {
|
||||
@@ -270,7 +274,7 @@ export function getNodeDetails(topologyUrlsById, currentTopologyId, options, nod
|
||||
let urlComponents = [getApiPath(), topologyUrl, '/', encodeURIComponent(obj.id)];
|
||||
if (currentTopologyId === obj.topologyId) {
|
||||
// Only forward filters for nodes in the current topology
|
||||
const optionsQuery = buildUrlQuery(options);
|
||||
const optionsQuery = buildUrlQuery(activeTopologyOptionsSelector(state), state);
|
||||
urlComponents = urlComponents.concat(['?', optionsQuery]);
|
||||
}
|
||||
const url = urlComponents.join('');
|
||||
|
||||
@@ -51,6 +51,22 @@
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.19), 0 6px 10px rgba(0, 0, 0, 0.23);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@extend .hideable;
|
||||
|
||||
background-color: white;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 2000;
|
||||
|
||||
&.faded { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.overlay-wrapper {
|
||||
align-items: center;
|
||||
background-color: fade-out($background-average-color, 0.1);
|
||||
@@ -208,16 +224,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.nodes-wrapper {
|
||||
@extend .hideable;
|
||||
|
||||
&.blurred { opacity: 0.2; }
|
||||
}
|
||||
|
||||
.time-travel {
|
||||
@extend .overlay-wrapper;
|
||||
display: block;
|
||||
right: 530px;
|
||||
z-index: 2001;
|
||||
|
||||
&-status {
|
||||
display: flex;
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"immutable-devtools": "0.0.7",
|
||||
"jest-cli": "19.0.2",
|
||||
"json-loader": "0.5.4",
|
||||
"mockdate": "^2.0.1",
|
||||
"node-sass": "4.5.2",
|
||||
"postcss-loader": "1.3.3",
|
||||
"react-addons-perf": "15.4.2",
|
||||
|
||||
@@ -1678,7 +1678,7 @@ d3-dispatch@1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
|
||||
|
||||
d3-drag@1:
|
||||
d3-drag@1, d3-drag@1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.0.4.tgz#a9c1609f11dd5530ae275ebd64377ec54efb9d8f"
|
||||
dependencies:
|
||||
@@ -1739,7 +1739,7 @@ d3-timer@1:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.5.tgz#b266d476c71b0d269e7ac5f352b410a3b6fe6ef0"
|
||||
|
||||
d3-transition@1:
|
||||
d3-transition@1, d3-transition@1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.0.4.tgz#e1a9ebae3869a9d9c2874ab00841fa8313ae5de5"
|
||||
dependencies:
|
||||
@@ -4070,6 +4070,10 @@ mixin-object@^2.0.1:
|
||||
dependencies:
|
||||
minimist "0.0.8"
|
||||
|
||||
mockdate@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-2.0.1.tgz#51bc309e2c4396600d56b6c23a6a0f4182943a36"
|
||||
|
||||
moment@2.18.1:
|
||||
version "2.18.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
|
||||
|
||||
Reference in New Issue
Block a user