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' => 'c7e29d9e', + 'rsrc/css/application/harbormaster/harbormaster.css' => '5dd4c2de', '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' => 'be6974cc', + 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '796a8803', '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' => 'c7e29d9e', + 'harbormaster-css' => '5dd4c2de', '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' => 'be6974cc', + 'javelin-behavior-harbormaster-log' => '796a8803', 'javelin-behavior-herald-rule-editor' => '7ebaeed3', 'javelin-behavior-high-security-warning' => 'a464fe03', 'javelin-behavior-history-install' => '7ee2b591', @@ -1535,6 +1535,9 @@ 'javelin-behavior', 'javelin-quicksand', ), + '796a8803' => array( + 'javelin-behavior', + ), '7a68dda3' => array( 'owners-path-editor', 'javelin-behavior', @@ -1889,9 +1892,6 @@ 'javelin-util', 'javelin-request', ), - 'be6974cc' => array( - 'javelin-behavior', - ), 'bea6e7f4' => array( 'javelin-install', 'javelin-dom', 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 @@ -22,14 +22,14 @@ if ($head_lines === null) { $head_lines = 8; } - $head_lines = min($head_lines, 100); + $head_lines = min($head_lines, 1024); $head_lines = max($head_lines, 0); $tail_lines = $request->getInt('tail'); if ($tail_lines === null) { $tail_lines = 16; } - $tail_lines = min($tail_lines, 100); + $tail_lines = min($tail_lines, 1024); $tail_lines = max($tail_lines, 0); $head_offset = $request->getInt('headOffset'); @@ -301,7 +301,6 @@ ); } - foreach ($views as $view) { if ($spacer) { $spacer['tail'] = $view['viewOffset']; @@ -360,6 +359,22 @@ } } + if ($log->getLive()) { + $last_view = last($views); + $last_line = last($last_view['viewData']); + if ($last_line) { + $last_offset = $last_line['offset']; + } else { + $last_offset = 0; + } + + $last_tail = $last_view['viewOffset'] + $last_view['viewLength']; + $show_live = ($last_tail === $log_size); + if ($show_live) { + $rows[] = $this->renderLiveRow($last_offset); + } + } + $table = phutil_tag( 'table', array( @@ -650,25 +665,66 @@ ), $expand_down), ); - $expand_row = phutil_tag('tr', array(), $expand_cells); - $expand_table = phutil_tag( + + return $this->renderActionTable($expand_cells); + } + + private function renderLiveRow($log_size) { + $icon_down = id(new PHUIIconView()) + ->setIcon('fa-chevron-down'); + + $follow = javelin_tag( + 'a', + array( + 'sigil' => 'harbormaster-log-expand harbormaster-log-live', + 'meta' => array( + 'headOffset' => $log_size, + 'head' => 0, + 'tail' => 1024, + 'live' => true, + ), + ), + array( + $icon_down, + ' ', + pht('Follow Log'), + ' ', + $icon_down, + )); + + $expand_cells = array( + phutil_tag( + 'td', + array( + 'class' => 'harbormaster-log-follow', + ), + $follow), + ); + + return $this->renderActionTable($expand_cells); + } + + private function renderActionTable(array $action_cells) { + $action_row = phutil_tag('tr', array(), $action_cells); + + $action_table = phutil_tag( 'table', array( 'class' => 'harbormaster-log-expand-table', ), - $expand_row); + $action_row); - $cells = array( + $format_cells = array( phutil_tag('th', array()), phutil_tag( 'td', array( 'class' => 'harbormaster-log-expand-cell', ), - $expand_table), + $action_table), ); - return phutil_tag('tr', array(), $cells); + return phutil_tag('tr', array(), $format_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 @@ -97,7 +97,6 @@ 'Helvetica Neue', Helvetica, Arial, sans-serif; } - .harbormaster-log-expand-up { text-align: right; width: 50%; @@ -107,6 +106,14 @@ margin: 0 0 0px 4px; } +.harbormaster-log-follow { + text-align: center; +} + +.harbormaster-log-follow .phui-icon-view { + margin: 0 4px; +} + .harbormaster-log-expand-mid { text-align: center; white-space: nowrap; 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,6 +5,7 @@ JX.behavior('harbormaster-log', function(config) { var contentNode = JX.$(config.contentNodeID); + var following = false; JX.DOM.listen(contentNode, 'click', 'harbormaster-log-expand', function(e) { if (!e.isNormalClick()) { @@ -13,23 +14,64 @@ e.kill(); - var row = e.getNode('tag:tr'); + expand(e.getTarget()); + }); + + function expand(node) { + var row = JX.DOM.findAbove(node, 'tr'); row = JX.DOM.findAbove(row, 'tr'); - var data = e.getNodeData('harbormaster-log-expand'); + var data = JX.Stratcom.getData(node); var uri = new JX.URI(config.renderURI) .addQueryParams(data); + if (data.live) { + following = true; + } + var request = new JX.Request(uri, function(r) { var result = JX.$H(r.markup).getNode(); var rows = [].slice.apply(result.firstChild.childNodes); + // If we're following the bottom of the log, the result always includes + // the last line from the previous render. Throw it away, then add the + // new data. + if (data.live && row.previousSibling) { + JX.DOM.remove(row.previousSibling); + } + JX.DOM.replace(row, rows); + + 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); + } }); request.send(); - }); + } + + function follow() { + if (!following) { + return; + } + + var live; + try { + live = JX.DOM.find(contentNode, 'a', 'harbormaster-log-live'); + } catch (e) { + return; + } + + expand(live); + } function onresponse(r) { JX.DOM.alterClass(contentNode, 'harbormaster-log-view-loading', false);