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 (
-
{versionUpdate &&
- - {pauseLabel !== '' && {pauseLabel}} - - 0; + const pauseTitle = isPaused ? + `Paused ${moment(this.props.updatePausedAt).fromNow()}` : + 'Pause updates (freezes the nodes in their current layout)'; + const pauseAction = isPaused ? this.props.clickResumeUpdate : this.props.clickPauseUpdate; + let pauseLabel = ''; + if (hasUpdates && isPaused) { + pauseLabel = `Paused +${updateCount}`; + } else if (hasUpdates && !isPaused) { + pauseLabel = `Resuming +${updateCount}`; + } else if (!hasUpdates && isPaused) { + pauseLabel = 'Paused'; + } + return (
- + + + + {false && } + + + + + {pauseLabel !== '' && {pauseLabel}} + +
); } } +function mapStateToProps(state) { + return { + updatePausedAt: state.get('updatePausedAt'), + }; +} -export default connect(null, { changeTopologyTimestamp })(TimelineControl); +export default connect( + mapStateToProps, + { + clickPauseUpdate, + clickResumeUpdate, + } +)(TimelineControl); diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 08d0fd42d6..fde90441df 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -48,13 +48,39 @@ } .overlay-wrapper { + align-items: center; background-color: fade-out($background-average-color, 0.1); border-radius: 4px; color: $text-tertiary-color; display: flex; font-size: 0.7rem; + justify-content: center; padding: 5px; position: absolute; + + a { + @extend .btn-opacity; + border: 1px solid transparent; + border-radius: 4px; + color: $text-secondary-color; + cursor: pointer; + padding: 4px 3px; + + .fa { + font-size: 150%; + position: relative; + top: 2px; + } + + &:hover { + border: 1px solid $text-tertiary-color; + } + + .active { + border: 1px solid $text-tertiary-color; + animation: blinking 1.5s infinite $base-ease; + } + } } .btn-opacity { @@ -142,12 +168,6 @@ bottom: 11px; right: 43px; - a { - @extend .btn-opacity; - color: $text-secondary-color; - cursor: pointer; - } - &-status { margin-right: 1em; } @@ -164,31 +184,6 @@ &-icon { margin-left: 0.5em; - padding: 4px 3px; - color: $text-color; - position: relative; - top: -1px; - border: 1px solid transparent; - border-radius: 4px; - - &:hover { - border: 1px solid $text-tertiary-color; - } - - .fa { - font-size: 150%; - position: relative; - top: 2px; - } - - &-active { - border: 1px solid $text-tertiary-color; - animation: blinking 1.5s infinite $base-ease; - } - } - - &-icon &-label { - margin-right: 0.5em; } .tooltip { @@ -198,14 +193,23 @@ } .timeline-control { - padding: 5px; - position: absolute; - bottom: 11px; - right: 500px; - color: $text-tertiary-color; - background-color: fade-out($background-average-color, .1); - font-size: 0.7rem; - display: flex; + @extend .overlay-wrapper; + left: calc(50vw - #{$timeline-control-width} / 2); + width: $timeline-control-width; + bottom: 0; + + + .status-info { + font-size: 125%; + + .fa { + margin-right: 5px; + font-size: 110%; + } + } + + .button { margin-left: 0.5em; } + input { margin: 0 8px; } } .topologies { @@ -1771,21 +1775,15 @@ .zoom-control { @extend .overlay-wrapper; - align-items: center; flex-direction: column; - padding: 10px 10px 5px; + padding: 5px 7px 0; bottom: 50px; right: 40px; - .zoom-in, .zoom-out { - @extend .btn-opacity; - color: $text-secondary-color; - cursor: pointer; - font-size: 150%; - } + a:hover { border-color: transparent; } .rc-slider { - margin: 10px 0; + margin: 5px 0; height: 60px; .rc-slider-step { cursor: pointer; } diff --git a/client/app/styles/_variables.scss b/client/app/styles/_variables.scss index 801f8738f4..345c6193cf 100644 --- a/client/app/styles/_variables.scss +++ b/client/app/styles/_variables.scss @@ -53,6 +53,8 @@ $link-opacity-default: 0.8; $search-border-color: transparent; $search-border-width: 1px; +$timeline-control-width: 285px; + /* specific elements */ $body-background-color: linear-gradient(30deg, $background-color 0%, $background-lighter-color 100%); $label-background-color: fade-out($background-average-color, .3);