Page MenuHomePhabricator

D19141.id45849.diff
No OneTemporary

D19141.id45849.diff

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' => 'f491c9f4',
+ 'rsrc/css/application/harbormaster/harbormaster.css' => 'fecac64f',
'rsrc/css/application/herald/herald-test.css' => 'a52e323e',
'rsrc/css/application/herald/herald.css' => 'cd8d0134',
'rsrc/css/application/maniphest/report.css' => '9b9580b7',
@@ -416,6 +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' => '0844f3c1',
'rsrc/js/application/herald/HeraldRuleEditor.js' => 'dca75c0e',
'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec',
'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3',
@@ -578,7 +579,7 @@
'font-fontawesome' => 'e838e088',
'font-lato' => 'c7ccd872',
'global-drag-and-drop-css' => 'b556a948',
- 'harbormaster-css' => 'f491c9f4',
+ 'harbormaster-css' => 'fecac64f',
'herald-css' => 'cd8d0134',
'herald-rule-editor' => 'dca75c0e',
'herald-test-css' => 'a52e323e',
@@ -635,6 +636,7 @@
'javelin-behavior-event-all-day' => 'b41537c9',
'javelin-behavior-fancy-datepicker' => 'ecf4e799',
'javelin-behavior-global-drag-and-drop' => '960f6a39',
+ 'javelin-behavior-harbormaster-log' => '0844f3c1',
'javelin-behavior-herald-rule-editor' => '7ebaeed3',
'javelin-behavior-high-security-warning' => 'a464fe03',
'javelin-behavior-history-install' => '7ee2b591',
@@ -960,6 +962,9 @@
'javelin-stratcom',
'javelin-workflow',
),
+ '0844f3c1' => array(
+ 'javelin-behavior',
+ ),
'08f4ccc3' => array(
'phui-oi-list-view-css',
),
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1230,6 +1230,7 @@
'HarbormasterBuildLogDownloadController' => 'applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php',
'HarbormasterBuildLogPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php',
'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.php',
+ 'HarbormasterBuildLogRenderController' => 'applications/harbormaster/controller/HarbormasterBuildLogRenderController.php',
'HarbormasterBuildLogTestCase' => 'applications/harbormaster/__tests__/HarbormasterBuildLogTestCase.php',
'HarbormasterBuildLogView' => 'applications/harbormaster/view/HarbormasterBuildLogView.php',
'HarbormasterBuildLogViewController' => 'applications/harbormaster/controller/HarbormasterBuildLogViewController.php',
@@ -6519,6 +6520,7 @@
'HarbormasterBuildLogDownloadController' => 'HarbormasterController',
'HarbormasterBuildLogPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'HarbormasterBuildLogRenderController' => 'HarbormasterController',
'HarbormasterBuildLogTestCase' => 'PhabricatorTestCase',
'HarbormasterBuildLogView' => 'AphrontView',
'HarbormasterBuildLogViewController' => 'HarbormasterController',
diff --git a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
--- a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
+++ b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
@@ -97,7 +97,10 @@
'buildkite/' => 'HarbormasterBuildkiteHookController',
),
'log/' => array(
- 'view/(?P<id>\d+)/' => 'HarbormasterBuildLogViewController',
+ 'view/(?P<id>\d+)/(?:\$(?P<lines>\d+(?:-\d+)?))?'
+ => 'HarbormasterBuildLogViewController',
+ 'render/(?P<id>\d+)/(?:\$(?P<lines>\d+(?:-\d+)?))?'
+ => 'HarbormasterBuildLogRenderController',
'download/(?P<id>\d+)/' => 'HarbormasterBuildLogDownloadController',
),
),
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php b/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php
@@ -0,0 +1,562 @@
+<?php
+
+final class HarbormasterBuildLogRenderController
+ extends HarbormasterController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $id = $request->getURIData('id');
+
+ $log = id(new HarbormasterBuildLogQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->executeOne();
+ if (!$log) {
+ return new Aphront404Response();
+ }
+
+ $log_size = $this->getTotalByteLength($log);
+
+ $head_lines = $request->getInt('head');
+ if ($head_lines === null) {
+ $head_lines = 8;
+ }
+ $head_lines = min($head_lines, 100);
+ $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 = max($tail_lines, 0);
+
+ $head_offset = $request->getInt('headOffset');
+ if ($head_offset === null) {
+ $head_offset = 0;
+ }
+
+ $tail_offset = $request->getInt('tailOffset');
+ if ($tail_offset === null) {
+ $tail_offset = $log_size;
+ }
+
+ // Figure out which ranges we're actually going to read. We'll read either
+ // one range (either just at the head, or just at the tail) or two ranges
+ // (one at the head and one at the tail).
+
+ // This gets a little bit tricky because: the ranges may overlap; we just
+ // want to do one big read if there is only a little bit of text left
+ // between the ranges; we may not know where the tail range ends; and we
+ // can only read forward from line map markers, not from any arbitrary
+ // position in the file.
+
+ $bytes_per_line = 140;
+ $body_lines = 8;
+
+ $views = array();
+ if ($head_lines > 0) {
+ $views[] = array(
+ 'offset' => $head_offset,
+ 'lines' => $head_lines,
+ 'direction' => 1,
+ );
+ }
+
+ if ($tail_lines > 0) {
+ $views[] = array(
+ 'offset' => $tail_offset,
+ 'lines' => $tail_lines,
+ 'direction' => -1,
+ );
+ }
+
+ $reads = $views;
+ foreach ($reads as $key => $read) {
+ $offset = $read['offset'];
+
+ $lines = $read['lines'];
+
+ $read_length = 0;
+ $read_length += ($lines * $bytes_per_line);
+ $read_length += ($body_lines * $bytes_per_line);
+
+ $direction = $read['direction'];
+ if ($direction < 0) {
+ $offset -= $read_length;
+ if ($offset < 0) {
+ $offset = 0;
+ $read_length = $log_size;
+ }
+ }
+
+ $position = $log->getReadPosition($offset);
+ list($position_offset, $position_line) = $position;
+ $read_length += ($offset - $position_offset);
+
+ $reads[$key]['fetchOffset'] = $position_offset;
+ $reads[$key]['fetchLength'] = $read_length;
+ $reads[$key]['fetchLine'] = $position_line;
+ }
+
+ $reads = $this->mergeOverlappingReads($reads);
+
+ foreach ($reads as $key => $read) {
+ $data = $log->loadData($read['fetchOffset'], $read['fetchLength']);
+
+ $offset = $read['fetchOffset'];
+ $line = $read['fetchLine'];
+ $lines = $this->getLines($data);
+ $line_data = array();
+ foreach ($lines as $line_text) {
+ $length = strlen($line_text);
+ $line_data[] = array(
+ 'offset' => $offset,
+ 'length' => $length,
+ 'line' => $line,
+ 'data' => $line_text,
+ );
+ $line += 1;
+ $offset += $length;
+ }
+
+ $reads[$key]['data'] = $data;
+ $reads[$key]['lines'] = $line_data;
+ }
+
+ foreach ($views as $view_key => $view) {
+ $anchor_byte = $view['offset'];
+
+ $data_key = null;
+ foreach ($reads as $read_key => $read) {
+ $s = $read['fetchOffset'];
+ $e = $s + $read['fetchLength'];
+
+ if (($s <= $anchor_byte) && ($e >= $anchor_byte)) {
+ $data_key = $read_key;
+ break;
+ }
+ }
+
+ if ($data_key === null) {
+ throw new Exception(
+ pht('Unable to find fetch!'));
+ }
+
+ $anchor_key = null;
+ foreach ($reads[$data_key]['lines'] as $line_key => $line) {
+ $s = $line['offset'];
+ $e = $s + $line['length'];
+ if (($s <= $anchor_byte) && ($e >= $anchor_byte)) {
+ $anchor_key = $line_key;
+ break;
+ }
+ }
+
+ if ($anchor_key === null) {
+ throw new Exception(
+ pht(
+ 'Unable to find lines.'));
+ }
+
+ if ($direction > 0) {
+ $slice_offset = $line_key;
+ } else {
+ $slice_offset = max(0, $line_key - ($view['lines'] - 1));
+ }
+ $slice_length = $view['lines'];
+
+ $views[$view_key] += array(
+ 'sliceKey' => $data_key,
+ 'sliceOffset' => $slice_offset,
+ 'sliceLength' => $slice_length,
+ );
+ }
+
+ foreach ($views as $view_key => $view) {
+ $slice_key = $view['sliceKey'];
+ $lines = array_slice(
+ $reads[$slice_key]['lines'],
+ $view['sliceOffset'],
+ $view['sliceLength']);
+
+ $data_offset = null;
+ $data_length = null;
+ foreach ($lines as $line) {
+ if ($data_offset === null) {
+ $data_offset = $line['offset'];
+ }
+ $data_length += $line['length'];
+ }
+
+ // If the view cursor starts in the middle of a line, we're going to
+ // strip part of the line.
+ $direction = $view['direction'];
+ if ($direction > 0) {
+ $view_offset = $view['offset'];
+ $view_length = $data_length;
+ if ($data_offset < $view_offset) {
+ $trim = ($view_offset - $data_offset);
+ $view_length -= $trim;
+ }
+ } else {
+ $view_offset = $data_offset;
+ $view_length = $data_length;
+ if ($data_offset + $data_length > $view['offset']) {
+ $view_length -= (($data_offset + $data_length) - $view['offset']);
+ }
+ }
+
+ $views[$view_key] += array(
+ 'viewOffset' => $view_offset,
+ 'viewLength' => $view_length,
+ );
+ }
+
+ $views = $this->mergeOverlappingViews($views);
+
+ foreach ($views as $view_key => $view) {
+ $slice_key = $view['sliceKey'];
+ $lines = array_slice(
+ $reads[$slice_key]['lines'],
+ $view['sliceOffset'],
+ $view['sliceLength']);
+
+ $view_offset = $view['viewOffset'];
+ foreach ($lines as $line_key => $line) {
+ $line_offset = $line['offset'];
+
+ if ($line_offset >= $view_offset) {
+ break;
+ }
+
+ $trim = ($view_offset - $line_offset);
+ $line_data = substr($line['data'], $trim);
+ if (!strlen($line_data)) {
+ unset($lines[$line_key]);
+ continue;
+ }
+
+ $lines[$line_key]['data'] = $line_data;
+ $lines[$line_key]['length'] = strlen($line_data);
+ $lines[$line_key]['offset'] += $trim;
+ break;
+ }
+
+ $view_end = $view['viewOffset'] + $view['viewLength'];
+ foreach ($lines as $line_key => $line) {
+ $line_end = $line['offset'] + $line['length'];
+ if ($line_end <= $view_end) {
+ break;
+ }
+
+ $trim = ($line_end - $view_end);
+ $line_data = substr($line['data'], -$trim);
+ if (!strlen($line_data)) {
+ unset($lines[$line_key]);
+ continue;
+ }
+
+ $lines[$line_key]['data'] = $line_data;
+ $lines[$line_key]['length'] = strlen($line_data);
+ }
+
+ $views[$view_key]['viewData'] = $lines;
+ }
+
+ $spacer = null;
+ $render = array();
+ foreach ($views as $view) {
+ if ($spacer) {
+ $spacer['tail'] = $view['viewOffset'];
+ $render[] = $spacer;
+ }
+
+ $render[] = $view;
+
+ $spacer = array(
+ 'spacer' => true,
+ 'head' => ($view['viewOffset'] + $view['viewLength']),
+ );
+ }
+
+ $uri = $log->getURI();
+ $highlight_range = $request->getURIData('lines');
+
+ $rows = array();
+ foreach ($render as $range) {
+ if (isset($range['spacer'])) {
+ $rows[] = phutil_tag(
+ 'tr',
+ array(),
+ array(
+ phutil_tag(
+ 'th',
+ array(),
+ null),
+ phutil_tag(
+ 'td',
+ array(),
+ array(
+ javelin_tag(
+ 'a',
+ array(
+ 'sigil' => 'harbormaster-log-expand',
+ 'meta' => array(
+ 'headOffset' => $range['head'],
+ 'tailOffset' => $range['tail'],
+ 'head' => 4,
+ ),
+ ),
+ 'Show Up ^^^^'),
+ '... '.($range['tail'] - $range['head']).' bytes ...',
+ javelin_tag(
+ 'a',
+ array(
+ 'sigil' => 'harbormaster-log-expand',
+ 'meta' => array(
+ 'headOffset' => $range['head'],
+ 'tailOffset' => $range['tail'],
+ 'tail' => 4,
+ ),
+ ),
+ 'Show Down VVVV'),
+ )),
+ ));
+ continue;
+ }
+
+ $lines = $range['viewData'];
+ foreach ($lines as $line) {
+ $display_line = ($line['line'] + 1);
+ $display_text = ($line['data']);
+
+ $display_line = phutil_tag(
+ 'a',
+ array(
+ 'href' => $uri.'$'.$display_line,
+ ),
+ $display_line);
+
+ $line_cell = phutil_tag('th', array(), $display_line);
+ $text_cell = phutil_tag('td', array(), $display_text);
+
+ $rows[] = phutil_tag(
+ 'tr',
+ array(),
+ array(
+ $line_cell,
+ $text_cell,
+ ));
+ }
+ }
+
+ $table = phutil_tag(
+ 'table',
+ array(
+ 'class' => 'harbormaster-log-table PhabricatorMonospaced',
+ ),
+ $rows);
+
+ // When this is a normal AJAX request, return the rendered log fragment
+ // in an AJAX payload.
+ if ($request->isAjax()) {
+ return id(new AphrontAjaxResponse())
+ ->setContent(
+ array(
+ 'markup' => hsprintf('%s', $table),
+ ));
+ }
+
+ // If the page is being accessed as a standalone page, present a
+ // readable version of the fragment for debugging.
+
+ require_celerity_resource('harbormaster-css');
+
+ $header = pht('Standalone Log Fragment');
+
+ $render_view = id(new PHUIObjectBoxView())
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setHeaderText($header)
+ ->appendChild($table);
+
+ $page_view = id(new PHUITwoColumnView())
+ ->setFooter($render_view);
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb(pht('Build Log %d', $log->getID()), $log->getURI())
+ ->addTextCrumb(pht('Fragment'))
+ ->setBorder(true);
+
+ return $this->newPage()
+ ->setTitle(
+ array(
+ pht('Build Log %d', $log->getID()),
+ pht('Standalone Fragment'),
+ ))
+ ->setCrumbs($crumbs)
+ ->appendChild($page_view);
+ }
+
+ private function getTotalByteLength(HarbormasterBuildLog $log) {
+ $total_bytes = $log->getByteLength();
+ if ($total_bytes) {
+ return (int)$total_bytes;
+ }
+
+ // TODO: Remove this after enough time has passed for installs to run
+ // log rebuilds or decide they don't care about older logs.
+
+ // Older logs don't have this data denormalized onto the log record unless
+ // an administrator has run `bin/harbormaster rebuild-log --all` or
+ // similar. Try to figure it out by summing up the size of each chunk.
+
+ // Note that the log may also be legitimately empty and have actual size
+ // zero.
+ $chunk = new HarbormasterBuildLogChunk();
+ $conn = $chunk->establishConnection('r');
+
+ $row = queryfx_one(
+ $conn,
+ 'SELECT SUM(size) total FROM %T WHERE logID = %d',
+ $chunk->getTableName(),
+ $log->getID());
+
+ return (int)$row['total'];
+ }
+
+ private function getLines($data) {
+ $parts = preg_split("/(\r\n|\r|\n)/", $data, 0, PREG_SPLIT_DELIM_CAPTURE);
+
+ if (last($parts) === '') {
+ array_pop($parts);
+ }
+
+ $lines = array();
+ for ($ii = 0; $ii < count($parts); $ii += 2) {
+ $line = $parts[$ii];
+ if (isset($parts[$ii + 1])) {
+ $line .= $parts[$ii + 1];
+ }
+ $lines[] = $line;
+ }
+
+ return $lines;
+ }
+
+
+ private function mergeOverlappingReads(array $reads) {
+ // Find planned reads which will overlap and merge them into a single
+ // larger read.
+
+ $uk = array_keys($reads);
+ $vk = array_keys($reads);
+
+ foreach ($uk as $ukey) {
+ foreach ($vk as $vkey) {
+ // Don't merge a range into itself, even though they do technically
+ // overlap.
+ if ($ukey === $vkey) {
+ continue;
+ }
+
+ $uread = idx($reads, $ukey);
+ if ($uread === null) {
+ continue;
+ }
+
+ $vread = idx($reads, $vkey);
+ if ($vread === null) {
+ continue;
+ }
+
+ $us = $uread['fetchOffset'];
+ $ue = $us + $uread['fetchLength'];
+
+ $vs = $vread['fetchOffset'];
+ $ve = $vs + $vread['fetchLength'];
+
+ if (($vs > $ue) || ($ve < $us)) {
+ continue;
+ }
+
+ $min = min($us, $vs);
+ $max = max($ue, $ve);
+
+ $reads[$ukey]['fetchOffset'] = $min;
+ $reads[$ukey]['fetchLength'] = ($max - $min);
+ $reads[$ukey]['fetchLine'] = min(
+ $uread['fetchLine'],
+ $vread['fetchLine']);
+
+ unset($reads[$vkey]);
+ }
+ }
+
+ return $reads;
+ }
+
+ private function mergeOverlappingViews(array $views) {
+ $uk = array_keys($views);
+ $vk = array_keys($views);
+
+ $body_lines = 8;
+ $body_bytes = ($body_lines * 140);
+
+ foreach ($uk as $ukey) {
+ foreach ($vk as $vkey) {
+ if ($ukey === $vkey) {
+ continue;
+ }
+
+ $uview = idx($views, $ukey);
+ if ($uview === null) {
+ continue;
+ }
+
+ $vview = idx($views, $vkey);
+ if ($vview === null) {
+ continue;
+ }
+
+ // If these views don't use the same line data, don't try to
+ // merge them.
+ if ($uview['sliceKey'] != $vview['sliceKey']) {
+ continue;
+ }
+
+ // If these views are overlapping or separated by only a few bytes,
+ // merge them into a single view.
+ $us = $uview['viewOffset'];
+ $ue = $us + $uview['viewLength'];
+
+ $vs = $vview['viewOffset'];
+ $ve = $vs + $vview['viewLength'];
+
+ $uss = $uview['sliceOffset'];
+ $use = $uss + $uview['sliceLength'];
+
+ $vss = $vview['sliceOffset'];
+ $vse = $vss + $vview['sliceLength'];
+
+ if ($ue <= $vs) {
+ if (($ue + $body_bytes) >= $vs) {
+ if (($use + $body_lines) >= $vss) {
+ $views[$ukey] = array(
+ 'sliceLength' => ($vse - $uss),
+ 'viewLength' => ($ve - $us),
+ ) + $views[$ukey];
+
+ unset($views[$vkey]);
+ continue;
+ }
+ }
+ }
+ }
+ }
+
+ return $views;
+ }
+
+}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildLogViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildLogViewController.php
--- a/src/applications/harbormaster/controller/HarbormasterBuildLogViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildLogViewController.php
@@ -4,8 +4,7 @@
extends HarbormasterController {
public function handleRequest(AphrontRequest $request) {
- $request = $this->getRequest();
- $viewer = $request->getUser();
+ $viewer = $this->getViewer();
$id = $request->getURIData('id');
@@ -21,7 +20,8 @@
$log_view = id(new HarbormasterBuildLogView())
->setViewer($viewer)
- ->setBuildLog($log);
+ ->setBuildLog($log)
+ ->setHighlightedLineRange($request->getURIData('lines'));
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(pht('Build Logs'))
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
@@ -129,6 +129,30 @@
$this->getID());
}
+ public function loadData($offset, $length) {
+ return substr($this->getLogText(), $offset, $length);
+ }
+
+ public function getReadPosition($read_offset) {
+ $position = array(0, 0);
+
+ $map = $this->getLineMap();
+ if (!$map) {
+ throw new Exception(pht('No line map.'));
+ }
+
+ list($map) = $map;
+ foreach ($map as $marker) {
+ list($offset, $count) = $marker;
+ if ($offset > $read_offset) {
+ break;
+ }
+ $position = $marker;
+ }
+
+ return $position;
+ }
+
public function getLogText() {
// TODO: Remove this method since it won't scale for big logs.
@@ -148,6 +172,15 @@
return "/harbormaster/log/view/{$id}/";
}
+ public function getRenderURI($lines) {
+ if (strlen($lines)) {
+ $lines = '$'.$lines;
+ }
+
+ $id = $this->getID();
+ return "/harbormaster/log/render/{$id}/{$lines}";
+ }
+
/* -( Chunks )------------------------------------------------------------- */
diff --git a/src/applications/harbormaster/view/HarbormasterBuildLogView.php b/src/applications/harbormaster/view/HarbormasterBuildLogView.php
--- a/src/applications/harbormaster/view/HarbormasterBuildLogView.php
+++ b/src/applications/harbormaster/view/HarbormasterBuildLogView.php
@@ -3,6 +3,7 @@
final class HarbormasterBuildLogView extends AphrontView {
private $log;
+ private $highlightedLineRange;
public function setBuildLog(HarbormasterBuildLog $log) {
$this->log = $log;
@@ -13,6 +14,15 @@
return $this->log;
}
+ public function setHighlightedLineRange($range) {
+ $this->highlightedLineRange = $range;
+ return $this;
+ }
+
+ public function getHighlightedLineRange() {
+ return $this->highlightedLineRange;
+ }
+
public function render() {
$viewer = $this->getViewer();
$log = $this->getBuildLog();
@@ -34,10 +44,28 @@
$header->addActionLink($download_button);
+ $content_id = celerity_generate_unique_node_id();
+ $content_div = javelin_tag(
+ 'div',
+ array(
+ 'id' => $content_id,
+ 'class' => 'harbormaster-log-view-loading',
+ ),
+ pht('Loading...'));
+
+ require_celerity_resource('harbormaster-css');
+
+ Javelin::initBehavior(
+ 'harbormaster-log',
+ array(
+ 'contentNodeID' => $content_id,
+ 'renderURI' => $log->getRenderURI($this->getHighlightedLineRange()),
+ ));
+
$box_view = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setHeader($header)
- ->appendChild('...');
+ ->appendChild($content_div);
return $box_view;
}
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
@@ -30,3 +30,41 @@
text-overflow: ellipsis;
color: {$lightgreytext};
}
+
+.harbormaster-log-view-loading {
+ padding: 8px;
+ text-align: center;
+ color: {$lightgreytext};
+}
+
+.harbormaster-log-table th {
+ background-color: {$paste.highlight};
+ border-right: 1px solid {$paste.border};
+
+ -moz-user-select: -moz-none;
+ -khtml-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.harbormaster-log-table th a {
+ display: block;
+ color: {$darkbluetext};
+ text-align: right;
+ padding: 2px 6px 1px 12px;
+}
+
+.harbormaster-log-table th a:hover {
+ background: {$paste.border};
+}
+
+.harbormaster-log-table td {
+ white-space: pre-wrap;
+ padding: 2px 8px 1px;
+ width: 100%;
+}
+
+.harbormaster-log-table tr.harbormaster-log-highlighted td {
+ background: {$paste.highlight};
+}
diff --git a/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js b/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js
new file mode 100644
--- /dev/null
+++ b/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js
@@ -0,0 +1,43 @@
+/**
+ * @provides javelin-behavior-harbormaster-log
+ * @requires javelin-behavior
+ */
+
+JX.behavior('harbormaster-log', function(config) {
+ var contentNode = JX.$(config.contentNodeID);
+
+ JX.DOM.listen(contentNode, 'click', 'harbormaster-log-expand', function(e) {
+ if (!e.isNormalClick()) {
+ return;
+ }
+
+ e.kill();
+
+ var row = e.getNode('tag:tr');
+ var data = e.getNodeData('harbormaster-log-expand');
+
+ var uri = new JX.URI(config.renderURI)
+ .addQueryParams(data);
+
+ var request = new JX.Request(uri, function(r) {
+ var result = JX.$H(r.markup).getNode();
+ var rows = JX.DOM.scry(result, 'tr');
+
+ JX.DOM.replace(row, rows);
+ });
+
+ request.send();
+ });
+
+ function onresponse(r) {
+ JX.DOM.alterClass(contentNode, 'harbormaster-log-view-loading', false);
+
+ JX.DOM.setContent(contentNode, JX.$H(r.markup));
+ }
+
+ var uri = new JX.URI(config.renderURI);
+
+ new JX.Request(uri, onresponse)
+ .send();
+
+});

File Metadata

Mime Type
text/plain
Expires
Sat, Mar 8, 3:36 PM (2 w, 8 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7383016
Default Alt Text
D19141.id45849.diff (26 KB)

Event Timeline