diff --git a/app/api_report.go b/app/api_report.go index 6dfbd153dd..2420a60e1a 100644 --- a/app/api_report.go +++ b/app/api_report.go @@ -13,7 +13,7 @@ import ( // Raw report handler func makeRawReportHandler(rep Reporter) CtxHandlerFunc { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - report, err := rep.Report(ctx, time.Now()) + report, err := rep.Report(ctx, 0) if err != nil { respondWith(w, http.StatusInternalServerError, err) return @@ -32,7 +32,7 @@ type probeDesc struct { // Probe handler func makeProbeHandler(rep Reporter) CtxHandlerFunc { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - rpt, err := rep.Report(ctx, time.Now()) + rpt, err := rep.Report(ctx, 0) if err != nil { respondWith(w, http.StatusInternalServerError, err) return diff --git a/app/api_topologies.go b/app/api_topologies.go index e2e7793608..ecff228ccf 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -7,7 +7,6 @@ import ( "sort" "strings" "sync" - "time" log "github.com/Sirupsen/logrus" "github.com/gorilla/mux" @@ -477,7 +476,7 @@ 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()) + report, err := rep.Report(ctx, 0) if err != nil { respondWith(w, http.StatusInternalServerError, err) return @@ -569,7 +568,7 @@ func (r *Registry) captureRenderer(rep Reporter, f rendererHandler) CtxHandlerFu http.NotFound(w, req) return } - rpt, err := rep.Report(ctx, time.Now()) + rpt, err := rep.Report(ctx, 0) if err != nil { respondWith(w, http.StatusInternalServerError, err) return diff --git a/app/api_topology.go b/app/api_topology.go index cd5e9e2a5c..43dda87f7a 100644 --- a/app/api_topology.go +++ b/app/api_topology.go @@ -100,18 +100,19 @@ func handleWebsocket( timestamp = time.Now() ) - log.Debugf("BLUBLUBLUBLU") if timestampStr := r.Form.Get("timestamp"); timestampStr != "" { - const ISO8601UTC = "2006-01-02T15:04:05Z" - timestamp, _ = time.Parse(ISO8601UTC, timestampStr) - log.Debugf("BLUBLUBLUBLU: %s %v", timestampStr, timestamp) + // Override the default current timestamp by the ISO8601 one explicitly provided by the UI. + timestamp, _ = time.Parse(time.RFC3339, timestampStr) } + // Use the time offset instead of a timestamp here so that the value + // can stay constant when simulating past reports (with normal speed). + timeOffset := time.Since(timestamp) rep.WaitOn(ctx, wait) defer rep.UnWait(ctx, wait) for { - report, err := rep.Report(ctx, timestamp) + report, err := rep.Report(ctx, timeOffset) if err != nil { log.Errorf("Error generating report: %v", err) return diff --git a/app/collector.go b/app/collector.go index 8a465605ee..9bd746cc5c 100644 --- a/app/collector.go +++ b/app/collector.go @@ -28,7 +28,7 @@ const reportQuantisationInterval = 3 * time.Second // Reporter is something that can produce reports on demand. It's a convenient // interface for parts of the app, and several experimental components. type Reporter interface { - Report(context.Context, time.Time) (report.Report, error) + Report(context.Context, time.Duration) (report.Report, error) WaitOn(context.Context, chan struct{}) UnWait(context.Context, chan struct{}) } @@ -118,14 +118,14 @@ func (c *collector) Add(_ context.Context, rpt report.Report, _ []byte) error { // Report returns a merged report over all added reports. It implements // Reporter. -func (c *collector) Report(_ context.Context, moment time.Time) (report.Report, error) { +func (c *collector) Report(_ context.Context, timeOffset time.Duration) (report.Report, error) { c.mtx.Lock() defer c.mtx.Unlock() // If the oldest report is still within range, // and there is a cached report, return that. if c.cached != nil && len(c.reports) > 0 { - oldest := moment.Add(-c.window) + oldest := mtime.Now().Add(-timeOffset - c.window) if c.timestamps[0].After(oldest) { return *c.cached, nil } @@ -191,7 +191,7 @@ type StaticCollector report.Report // Report returns a merged report over all added reports. It implements // Reporter. -func (c StaticCollector) Report(context.Context, time.Time) (report.Report, error) { +func (c StaticCollector) Report(context.Context, time.Duration) (report.Report, error) { return report.Report(c), nil } diff --git a/app/multitenant/aws_collector.go b/app/multitenant/aws_collector.go index 3ccb6b7a1b..61c2896f9a 100644 --- a/app/multitenant/aws_collector.go +++ b/app/multitenant/aws_collector.go @@ -297,9 +297,9 @@ func (c *awsCollector) getReports(ctx context.Context, reportKeys []string) ([]r return reports, nil } -func (c *awsCollector) Report(ctx context.Context, pointInTime time.Time) (report.Report, error) { +func (c *awsCollector) Report(ctx context.Context, timeOffset time.Duration) (report.Report, error) { var ( - end = pointInTime + end = time.Now().Add(-timeOffset) start = end.Add(-c.window) rowStart = start.UnixNano() / time.Hour.Nanoseconds() rowEnd = end.UnixNano() / time.Hour.Nanoseconds() diff --git a/app/router.go b/app/router.go index 90480a0944..cfc8e93de9 100644 --- a/app/router.go +++ b/app/router.go @@ -9,7 +9,6 @@ import ( "net/url" "strings" "sync" - "time" "github.com/PuerkitoBio/ghost/handlers" log "github.com/Sirupsen/logrus" @@ -180,7 +179,7 @@ func NewVersion(version, downloadURL string) { func apiHandler(rep Reporter) CtxHandlerFunc { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { - report, err := rep.Report(ctx, time.Now()) + report, err := rep.Report(ctx, 0) if err != nil { respondWith(w, http.StatusInternalServerError, err) return diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js index f061f83156..6cd8bf3e53 100644 --- a/client/app/scripts/charts/nodes-layout.js +++ b/client/app/scripts/charts/nodes-layout.js @@ -422,7 +422,7 @@ export function doLayout(immNodes, immEdges, opts) { const cacheId = buildTopologyCacheId(options.topologyId, options.topologyOptions); // one engine and node and edge caches per topology, to keep renderings similar - if (options.noCache || !topologyCaches[cacheId]) { + if (true || options.noCache || !topologyCaches[cacheId]) { topologyCaches[cacheId] = { nodeCache: makeMap(), edgeCache: makeMap(), diff --git a/client/app/scripts/components/editable-time.js b/client/app/scripts/components/editable-time.js new file mode 100644 index 0000000000..62bc96ff34 --- /dev/null +++ b/client/app/scripts/components/editable-time.js @@ -0,0 +1,90 @@ +import React from 'react'; +import moment from 'moment'; +import { connect } from 'react-redux'; + +import { ENTER_KEY_CODE } from '../constants/key-codes'; +import { changeTopologyTimestamp } from '../actions/app-actions'; + + +class EditableTime extends React.PureComponent { + constructor(props, context) { + super(props, context); + + this.state = { + value: '', + editing: false, + timeOffset: 0, + }; + + this.handleFocus = this.handleFocus.bind(this); + this.handleBlur = this.handleBlur.bind(this); + this.handleKeyUp = this.handleKeyUp.bind(this); + this.handleChange = this.handleChange.bind(this); + this.saveInputRef = this.saveInputRef.bind(this); + } + + componentDidMount() { + this.timer = setInterval(() => { + if (!this.state.editing) { + this.setState(this.getFreshState()); + } + }, 100); + } + + componentWillUnmount() { + clearInterval(this.timer); + } + + handleFocus() { + this.setState({ editing: true }); + } + + handleBlur() { + this.setState({ editing: false }); + } + + handleChange(ev) { + this.setState({ value: ev.target.value }); + } + + handleKeyUp(ev) { + if (ev.keyCode === ENTER_KEY_CODE) { + const { value } = this.state; + const timeOffset = moment.duration(moment().diff(value)); + this.props.changeTopologyTimestamp(value); + + this.setState({ timeOffset }); + this.inputRef.blur(); + + console.log('QUERY TOPOLOGY AT: ', value, timeOffset); + } + } + + saveInputRef(ref) { + this.inputRef = ref; + } + + getFreshState() { + const { timeOffset } = this.state; + const time = moment().utc().subtract(timeOffset); + return { value: time.toISOString() }; + } + + render() { + return ( + + + + ); + } +} + +export default connect(null, { changeTopologyTimestamp })(EditableTime); diff --git a/client/app/scripts/components/footer.js b/client/app/scripts/components/footer.js index 5da5c3c33e..410edc45f8 100644 --- a/client/app/scripts/components/footer.js +++ b/client/app/scripts/components/footer.js @@ -1,15 +1,11 @@ import React from 'react'; import { connect } from 'react-redux'; -import moment from 'moment'; import Plugins from './plugins'; -import { getUpdateBufferSize } from '../utils/update-buffer-utils'; import { trackMixpanelEvent } from '../utils/tracking-utils'; import { clickDownloadGraph, clickForceRelayout, - clickPauseUpdate, - clickResumeUpdate, toggleHelp, toggleTroubleshootingMenu, setContrastMode @@ -38,38 +34,18 @@ class Footer extends React.Component { } render() { - const { hostname, updatePausedAt, version, versionUpdate, contrastMode } = this.props; + const { hostname, version, versionUpdate, contrastMode } = this.props; const otherContrastModeTitle = contrastMode ? 'Switch to normal contrast' : 'Switch to high contrast'; const forceRelayoutTitle = 'Force re-layout (might reduce edge crossings, ' + 'but may shift nodes around)'; - - // pause button - const isPaused = updatePausedAt !== null; - const updateCount = getUpdateBufferSize(); - const hasUpdates = updateCount > 0; - const pausedAgo = moment(updatePausedAt).fromNow(); - const pauseTitle = isPaused - ? `Paused ${pausedAgo}` : 'Pause updates (freezes the nodes in their current layout)'; - const pauseAction = isPaused ? this.props.clickResumeUpdate : this.props.clickPauseUpdate; - const pauseClassName = isPaused ? 'footer-icon footer-icon-active' : 'footer-icon'; - let pauseLabel = ''; - if (hasUpdates && isPaused) { - pauseLabel = `Paused +${updateCount}`; - } else if (hasUpdates && !isPaused) { - pauseLabel = `Resuming +${updateCount}`; - } else if (!hasUpdates && isPaused) { - pauseLabel = 'Paused'; - } - const versionUpdateTitle = versionUpdate ? `New version available: ${versionUpdate.version}. Click to download` : ''; return (