diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -78,7 +78,7 @@ 'rsrc/css/application/feed/feed.css' => 'ecd4ec57', 'rsrc/css/application/files/global-drag-and-drop.css' => 'b556a948', 'rsrc/css/application/flag/flag.css' => 'bba8f811', - 'rsrc/css/application/harbormaster/harbormaster.css' => 'cd73d427', + 'rsrc/css/application/harbormaster/harbormaster.css' => '730a4a3c', 'rsrc/css/application/herald/herald-test.css' => 'a52e323e', 'rsrc/css/application/herald/herald.css' => 'cd8d0134', 'rsrc/css/application/maniphest/report.css' => '9b9580b7', @@ -416,7 +416,7 @@ 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef', 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', - 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => 'ab1173d1', + 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '191b4909', 'rsrc/js/application/herald/HeraldRuleEditor.js' => 'dca75c0e', 'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec', 'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3', @@ -579,7 +579,7 @@ 'font-fontawesome' => 'e838e088', 'font-lato' => 'c7ccd872', 'global-drag-and-drop-css' => 'b556a948', - 'harbormaster-css' => 'cd73d427', + 'harbormaster-css' => '730a4a3c', 'herald-css' => 'cd8d0134', 'herald-rule-editor' => 'dca75c0e', 'herald-test-css' => 'a52e323e', @@ -636,7 +636,7 @@ 'javelin-behavior-event-all-day' => 'b41537c9', 'javelin-behavior-fancy-datepicker' => 'ecf4e799', 'javelin-behavior-global-drag-and-drop' => '960f6a39', - 'javelin-behavior-harbormaster-log' => 'ab1173d1', + 'javelin-behavior-harbormaster-log' => '191b4909', 'javelin-behavior-herald-rule-editor' => '7ebaeed3', 'javelin-behavior-high-security-warning' => 'a464fe03', 'javelin-behavior-history-install' => '7ee2b591', @@ -1022,6 +1022,9 @@ '185bbd53' => array( 'javelin-install', ), + '191b4909' => array( + 'javelin-behavior', + ), '1ad0a787' => array( 'javelin-install', 'javelin-reactor', @@ -1765,9 +1768,6 @@ 'javelin-util', 'phabricator-prefab', ), - 'ab1173d1' => array( - 'javelin-behavior', - ), 'ab2f381b' => array( 'javelin-request', 'javelin-behavior', diff --git a/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php b/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php --- a/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php @@ -720,12 +720,16 @@ private function renderLiveRow($log_size) { $icon_down = id(new PHUIIconView()) - ->setIcon('fa-chevron-down'); + ->setIcon('fa-angle-double-down'); + + $icon_pause = id(new PHUIIconView()) + ->setIcon('fa-pause'); $follow = javelin_tag( 'a', array( 'sigil' => 'harbormaster-log-expand harbormaster-log-live', + 'class' => 'harbormaster-log-follow-start', 'meta' => array( 'headOffset' => $log_size, 'head' => 0, @@ -737,8 +741,21 @@ $icon_down, ' ', pht('Follow Log'), + )); + + $stop_following = javelin_tag( + 'a', + array( + 'sigil' => 'harbormaster-log-expand', + 'class' => 'harbormaster-log-follow-stop', + 'meta' => array( + 'stop' => true, + ), + ), + array( + $icon_pause, ' ', - $icon_down, + pht('Stop Following Log'), )); $expand_cells = array( @@ -747,7 +764,10 @@ array( 'class' => 'harbormaster-log-follow', ), - $follow), + array( + $follow, + $stop_following, + )), ); return $this->renderActionTable($expand_cells); diff --git a/webroot/rsrc/css/application/harbormaster/harbormaster.css b/webroot/rsrc/css/application/harbormaster/harbormaster.css --- a/webroot/rsrc/css/application/harbormaster/harbormaster.css +++ b/webroot/rsrc/css/application/harbormaster/harbormaster.css @@ -139,3 +139,31 @@ .harbormaster-log-expand-down .phui-icon-view { margin: 0 4px 0 0; } + + +.harbormaster-log-following .harbormaster-log-table + .harbormaster-log-follow-start { + display: none; +} + +.harbormaster-log-table .harbormaster-log-follow-stop { + display: none; +} + +.harbormaster-log-following .harbormaster-log-table + .harbormaster-log-follow-stop { + display: block; +} + +.harbormaster-log-appear > td { + animation: harbormaster-fade-in 1s linear; +} + +@keyframes harbormaster-fade-in { + 0% { + opacity: 0.5; + } + 100% { + opacity: 1.0; + } +} diff --git a/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js b/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js --- a/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js +++ b/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js @@ -5,7 +5,9 @@ JX.behavior('harbormaster-log', function(config) { var contentNode = JX.$(config.contentNodeID); + var following = false; + var autoscroll = false; JX.DOM.listen(contentNode, 'click', 'harbormaster-log-expand', function(e) { if (!e.isNormalClick()) { @@ -14,20 +16,29 @@ e.kill(); - expand(e.getTarget()); + expand(e.getTarget(), true); }); - function expand(node) { + function expand(node, is_action) { var row = JX.DOM.findAbove(node, 'tr'); row = JX.DOM.findAbove(row, 'tr'); var data = JX.Stratcom.getData(node); + if (data.stop) { + following = false; + autoscroll = false; + JX.DOM.alterClass(contentNode, 'harbormaster-log-following', false); + return; + } + var uri = new JX.URI(config.renderURI) .addQueryParams(data); - if (data.live) { + if (data.live && is_action) { following = true; + autoscroll = true; + JX.DOM.alterClass(contentNode, 'harbormaster-log-following', true); } var request = new JX.Request(uri, function(r) { @@ -46,18 +57,34 @@ if (data.live) { // If this was a live follow, scroll the new data into view. This is // probably intensely annoying in practice but seems cool for now. - var last_row = rows[rows.length - 1]; - var tail_pos = JX.$V(last_row).y + JX.Vector.getDim(last_row).y; - var view_y = JX.Vector.getViewport().y; - JX.DOM.scrollToPosition(null, (tail_pos - view_y) + 32); - - setTimeout(follow, 500); + if (autoscroll) { + var last_row = rows[rows.length - 1]; + var tail_pos = JX.$V(last_row).y + JX.Vector.getDim(last_row).y; + var view_y = JX.Vector.getViewport().y; + JX.DOM.scrollToPosition(null, (tail_pos - view_y) + 32); + + // This will fire a scroll event, but we want to keep autoscroll + // enabled until we see an explicit scroll event by the user. + setTimeout(function() { autoscroll = true; }, 0); + } + + setTimeout(follow, 2000); + + for (var ii = 1; ii < (rows.length - 1); ii++) { + JX.DOM.alterClass(rows[ii], 'harbormaster-log-appear', true); + } } }); request.send(); } + // If the user explicitly scrolls while following a log, keep live updating + // it but stop following it with the scrollbar. + JX.Stratcom.listen('scroll', null, function() { + autoscroll = false; + }); + function follow() { if (!following) { return;