From 114ed6c7feeafddfc0a92944990e9efbc66bd136 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 28 Jan 2013 18:45:32 -0800 Subject: [PATCH] DarkConsole: fix rendering, move request log, load over ajax Summary: This accomplishes three major goals: # Fixes phutil_render_tag -> phutil_tag callsites in DarkConsole. # Moves the Ajax request log to a new panel on the left. This panel (and the tabs panel) get scrollbars when they get large, instead of making the page constantly scroll down. # Loads the panel content over ajax, instead of dumping it into the page body / ajax response body. I've been planning to do this for about 3 years, which is why the plugins are architected the way they are. This should make debugging easier by making response bodies not be 50%+ darkconsole stuff. Additionally, load the plugins dynamically (the old method predates library maps and PhutilSymbolLoader). Test Plan: {F30675} - Switched between requests and tabs, reloaded page, saw same tab. - Used "analyze queries", "profile page", triggered errors. - Verified page does not load anything by default if dark console is closed with Charles. - Generally banged on it a bit. Reviewers: vrana, btrahan, chad Reviewed By: vrana CC: aran Maniphest Tasks: T2432 Differential Revision: https://secure.phabricator.com/D4692 --- scripts/celerity_mapper.php | 1 - src/__celerity_resource_map__.php | 155 ++++----- src/__phutil_library_map__.php | 2 + ...AphrontDefaultApplicationConfiguration.php | 5 +- src/aphront/console/DarkConsoleCore.php | 207 +++--------- .../console/DarkConsoleDataController.php | 69 ++++ .../plugin/DarkConsoleErrorLogPlugin.php | 25 +- .../console/plugin/DarkConsoleEventPlugin.php | 2 +- .../console/plugin/DarkConsolePlugin.php | 21 +- .../plugin/DarkConsoleRequestPlugin.php | 3 +- .../plugin/DarkConsoleServicesPlugin.php | 11 +- .../plugin/DarkConsoleXHProfPlugin.php | 32 +- src/aphront/response/AphrontAjaxResponse.php | 6 +- src/view/page/PhabricatorStandardPageView.php | 11 +- webroot/rsrc/css/aphront/dark-console.css | 123 ++++--- webroot/rsrc/image/darkload.gif | Bin 0 -> 9427 bytes .../core/behavior-dark-console-ajax.js | 49 --- .../application/core/behavior-dark-console.js | 319 +++++++++++------- 18 files changed, 560 insertions(+), 481 deletions(-) create mode 100644 src/aphront/console/DarkConsoleDataController.php create mode 100644 webroot/rsrc/image/darkload.gif delete mode 100644 webroot/rsrc/js/application/core/behavior-dark-console-ajax.js diff --git a/scripts/celerity_mapper.php b/scripts/celerity_mapper.php index fd24b0eaa4..93867c86ff 100755 --- a/scripts/celerity_mapper.php +++ b/scripts/celerity_mapper.php @@ -166,7 +166,6 @@ $package_spec = array( 'javelin-behavior-maniphest-subpriority-editor', ), 'darkconsole.pkg.js' => array( - 'javelin-behavior-dark-console-ajax', 'javelin-behavior-dark-console', 'javelin-behavior-error-log', ), diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 6edcdb0feb..da47577120 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -42,6 +42,13 @@ celerity_register_resource_map(array( 'disk' => '/rsrc/image/credit_cards.png', 'type' => 'png', ), + '/rsrc/image/darkload.gif' => + array( + 'hash' => '3a52cb7145d6e70f461fed21273117f2', + 'uri' => '/res/3a52cb71/rsrc/image/darkload.gif', + 'disk' => '/rsrc/image/darkload.gif', + 'type' => 'gif', + ), '/rsrc/image/divot.png' => array( 'hash' => '3be267bd11ea375bf68e808893718e0e', @@ -598,7 +605,7 @@ celerity_register_resource_map(array( ), 'aphront-dark-console-css' => array( - 'uri' => '/res/1e1f78d4/rsrc/css/aphront/dark-console.css', + 'uri' => '/res/63841304/rsrc/css/aphront/dark-console.css', 'type' => 'css', 'requires' => array( @@ -1057,7 +1064,7 @@ celerity_register_resource_map(array( ), 'javelin-behavior-aphront-form-disable-on-submit' => array( - 'uri' => '/res/ca54e8b9/rsrc/js/application/core/behavior-form.js', + 'uri' => '/res/70fd43fd/rsrc/js/application/core/behavior-form.js', 'type' => 'js', 'requires' => array( @@ -1145,7 +1152,7 @@ celerity_register_resource_map(array( ), 'javelin-behavior-dark-console' => array( - 'uri' => '/res/aa6f8a71/rsrc/js/application/core/behavior-dark-console.js', + 'uri' => '/res/c3e8a3d8/rsrc/js/application/core/behavior-dark-console.js', 'type' => 'js', 'requires' => array( @@ -1155,21 +1162,9 @@ celerity_register_resource_map(array( 3 => 'javelin-dom', 4 => 'javelin-request', 5 => 'phabricator-keyboard-shortcut', - 6 => 'javelin-behavior-dark-console-ajax', ), 'disk' => '/rsrc/js/application/core/behavior-dark-console.js', ), - 'javelin-behavior-dark-console-ajax' => - array( - 'uri' => '/res/ac3ab63a/rsrc/js/application/core/behavior-dark-console-ajax.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - ), - 'disk' => '/rsrc/js/application/core/behavior-dark-console-ajax.js', - ), 'javelin-behavior-device' => array( 'uri' => '/res/a10b851b/rsrc/js/application/core/behavior-device.js', @@ -1493,7 +1488,7 @@ celerity_register_resource_map(array( ), 'javelin-behavior-lightbox-attachments' => array( - 'uri' => '/res/5efba371/rsrc/js/application/core/behavior-lightbox-attachments.js', + 'uri' => '/res/08f5e202/rsrc/js/application/core/behavior-lightbox-attachments.js', 'type' => 'js', 'requires' => array( @@ -1986,7 +1981,7 @@ celerity_register_resource_map(array( ), 'javelin-dom' => array( - 'uri' => '/res/2826c532/rsrc/js/javelin/lib/DOM.js', + 'uri' => '/res/459f3c08/rsrc/js/javelin/lib/DOM.js', 'type' => 'js', 'requires' => array( @@ -3393,7 +3388,7 @@ celerity_register_resource_map(array( 'uri' => '/res/pkg/023adc14/core.pkg.css', 'type' => 'css', ), - '66dca903' => + 'b3c1b6e7' => array( 'name' => 'core.pkg.js', 'symbols' => @@ -3432,19 +3427,18 @@ celerity_register_resource_map(array( 31 => 'javelin-behavior-global-drag-and-drop', 32 => 'javelin-behavior-phabricator-home-reveal-tiles', ), - 'uri' => '/res/pkg/66dca903/core.pkg.js', + 'uri' => '/res/pkg/b3c1b6e7/core.pkg.js', 'type' => 'js', ), - '8edbada5' => + '032118cf' => array( 'name' => 'darkconsole.pkg.js', 'symbols' => array( - 0 => 'javelin-behavior-dark-console-ajax', - 1 => 'javelin-behavior-dark-console', - 2 => 'javelin-behavior-error-log', + 0 => 'javelin-behavior-dark-console', + 1 => 'javelin-behavior-error-log', ), - 'uri' => '/res/pkg/8edbada5/darkconsole.pkg.js', + 'uri' => '/res/pkg/032118cf/darkconsole.pkg.js', 'type' => 'js', ), 'ec01d039' => @@ -3521,7 +3515,7 @@ celerity_register_resource_map(array( 'uri' => '/res/pkg/f96657b8/diffusion.pkg.js', 'type' => 'js', ), - 'fbeded59' => + '1c6f020b' => array( 'name' => 'javelin.pkg.js', 'symbols' => @@ -3546,7 +3540,7 @@ celerity_register_resource_map(array( 17 => 'javelin-typeahead-ondemand-source', 18 => 'javelin-tokenizer', ), - 'uri' => '/res/pkg/fbeded59/javelin.pkg.js', + 'uri' => '/res/pkg/1c6f020b/javelin.pkg.js', 'type' => 'js', ), 'e30a3fa8' => @@ -3608,18 +3602,17 @@ celerity_register_resource_map(array( 'diffusion-icons-css' => 'c8ce2d88', 'global-drag-and-drop-css' => '023adc14', 'inline-comment-summary-css' => 'ec01d039', - 'javelin-aphlict' => '66dca903', - 'javelin-behavior' => 'fbeded59', - 'javelin-behavior-aphlict-dropdown' => '66dca903', - 'javelin-behavior-aphlict-listen' => '66dca903', - 'javelin-behavior-aphront-basic-tokenizer' => '66dca903', + 'javelin-aphlict' => 'b3c1b6e7', + 'javelin-behavior' => '1c6f020b', + 'javelin-behavior-aphlict-dropdown' => 'b3c1b6e7', + 'javelin-behavior-aphlict-listen' => 'b3c1b6e7', + 'javelin-behavior-aphront-basic-tokenizer' => 'b3c1b6e7', 'javelin-behavior-aphront-drag-and-drop' => '310cd201', 'javelin-behavior-aphront-drag-and-drop-textarea' => '310cd201', - 'javelin-behavior-aphront-form-disable-on-submit' => '66dca903', + 'javelin-behavior-aphront-form-disable-on-submit' => 'b3c1b6e7', 'javelin-behavior-audit-preview' => 'f96657b8', - 'javelin-behavior-dark-console' => '8edbada5', - 'javelin-behavior-dark-console-ajax' => '8edbada5', - 'javelin-behavior-device' => '66dca903', + 'javelin-behavior-dark-console' => '032118cf', + 'javelin-behavior-device' => 'b3c1b6e7', 'javelin-behavior-differential-accept-with-errors' => '310cd201', 'javelin-behavior-differential-add-reviewers-and-ccs' => '310cd201', 'javelin-behavior-differential-comment-jump' => '310cd201', @@ -3634,84 +3627,84 @@ celerity_register_resource_map(array( 'javelin-behavior-differential-user-select' => '310cd201', 'javelin-behavior-diffusion-commit-graph' => 'f96657b8', 'javelin-behavior-diffusion-pull-lastmodified' => 'f96657b8', - 'javelin-behavior-error-log' => '8edbada5', - 'javelin-behavior-global-drag-and-drop' => '66dca903', - 'javelin-behavior-konami' => '66dca903', - 'javelin-behavior-lightbox-attachments' => '66dca903', + 'javelin-behavior-error-log' => '032118cf', + 'javelin-behavior-global-drag-and-drop' => 'b3c1b6e7', + 'javelin-behavior-konami' => 'b3c1b6e7', + 'javelin-behavior-lightbox-attachments' => 'b3c1b6e7', 'javelin-behavior-maniphest-batch-selector' => '7707de41', 'javelin-behavior-maniphest-subpriority-editor' => '7707de41', 'javelin-behavior-maniphest-transaction-controls' => '7707de41', 'javelin-behavior-maniphest-transaction-expand' => '7707de41', 'javelin-behavior-maniphest-transaction-preview' => '7707de41', - 'javelin-behavior-phabricator-active-nav' => '66dca903', - 'javelin-behavior-phabricator-autofocus' => '66dca903', - 'javelin-behavior-phabricator-home-reveal-tiles' => '66dca903', - 'javelin-behavior-phabricator-keyboard-shortcuts' => '66dca903', - 'javelin-behavior-phabricator-nav' => '66dca903', + 'javelin-behavior-phabricator-active-nav' => 'b3c1b6e7', + 'javelin-behavior-phabricator-autofocus' => 'b3c1b6e7', + 'javelin-behavior-phabricator-home-reveal-tiles' => 'b3c1b6e7', + 'javelin-behavior-phabricator-keyboard-shortcuts' => 'b3c1b6e7', + 'javelin-behavior-phabricator-nav' => 'b3c1b6e7', 'javelin-behavior-phabricator-object-selector' => '310cd201', - 'javelin-behavior-phabricator-oncopy' => '66dca903', - 'javelin-behavior-phabricator-remarkup-assist' => '66dca903', - 'javelin-behavior-phabricator-search-typeahead' => '66dca903', - 'javelin-behavior-phabricator-tooltips' => '66dca903', - 'javelin-behavior-phabricator-watch-anchor' => '66dca903', - 'javelin-behavior-refresh-csrf' => '66dca903', + 'javelin-behavior-phabricator-oncopy' => 'b3c1b6e7', + 'javelin-behavior-phabricator-remarkup-assist' => 'b3c1b6e7', + 'javelin-behavior-phabricator-search-typeahead' => 'b3c1b6e7', + 'javelin-behavior-phabricator-tooltips' => 'b3c1b6e7', + 'javelin-behavior-phabricator-watch-anchor' => 'b3c1b6e7', + 'javelin-behavior-refresh-csrf' => 'b3c1b6e7', 'javelin-behavior-repository-crossreference' => '310cd201', - 'javelin-behavior-toggle-class' => '66dca903', - 'javelin-behavior-workflow' => '66dca903', - 'javelin-dom' => 'fbeded59', - 'javelin-event' => 'fbeded59', - 'javelin-install' => 'fbeded59', - 'javelin-json' => 'fbeded59', - 'javelin-mask' => 'fbeded59', - 'javelin-request' => 'fbeded59', - 'javelin-resource' => 'fbeded59', - 'javelin-stratcom' => 'fbeded59', - 'javelin-tokenizer' => 'fbeded59', - 'javelin-typeahead' => 'fbeded59', - 'javelin-typeahead-normalizer' => 'fbeded59', - 'javelin-typeahead-ondemand-source' => 'fbeded59', - 'javelin-typeahead-preloaded-source' => 'fbeded59', - 'javelin-typeahead-source' => 'fbeded59', - 'javelin-uri' => 'fbeded59', - 'javelin-util' => 'fbeded59', - 'javelin-vector' => 'fbeded59', - 'javelin-workflow' => 'fbeded59', + 'javelin-behavior-toggle-class' => 'b3c1b6e7', + 'javelin-behavior-workflow' => 'b3c1b6e7', + 'javelin-dom' => '1c6f020b', + 'javelin-event' => '1c6f020b', + 'javelin-install' => '1c6f020b', + 'javelin-json' => '1c6f020b', + 'javelin-mask' => '1c6f020b', + 'javelin-request' => '1c6f020b', + 'javelin-resource' => '1c6f020b', + 'javelin-stratcom' => '1c6f020b', + 'javelin-tokenizer' => '1c6f020b', + 'javelin-typeahead' => '1c6f020b', + 'javelin-typeahead-normalizer' => '1c6f020b', + 'javelin-typeahead-ondemand-source' => '1c6f020b', + 'javelin-typeahead-preloaded-source' => '1c6f020b', + 'javelin-typeahead-source' => '1c6f020b', + 'javelin-uri' => '1c6f020b', + 'javelin-util' => '1c6f020b', + 'javelin-vector' => '1c6f020b', + 'javelin-workflow' => '1c6f020b', 'lightbox-attachment-css' => '023adc14', 'maniphest-task-summary-css' => 'e30a3fa8', 'maniphest-transaction-detail-css' => 'e30a3fa8', - 'phabricator-busy' => '66dca903', + 'phabricator-busy' => 'b3c1b6e7', 'phabricator-content-source-view-css' => 'ec01d039', 'phabricator-core-buttons-css' => '023adc14', 'phabricator-core-css' => '023adc14', 'phabricator-crumbs-view-css' => '023adc14', 'phabricator-directory-css' => '023adc14', 'phabricator-drag-and-drop-file-upload' => '310cd201', - 'phabricator-dropdown-menu' => '66dca903', - 'phabricator-file-upload' => '66dca903', + 'phabricator-dropdown-menu' => 'b3c1b6e7', + 'phabricator-file-upload' => 'b3c1b6e7', 'phabricator-filetree-view-css' => '023adc14', 'phabricator-flag-css' => '023adc14', 'phabricator-form-view-css' => '023adc14', 'phabricator-header-view-css' => '023adc14', 'phabricator-jump-nav' => '023adc14', - 'phabricator-keyboard-shortcut' => '66dca903', - 'phabricator-keyboard-shortcut-manager' => '66dca903', + 'phabricator-keyboard-shortcut' => 'b3c1b6e7', + 'phabricator-keyboard-shortcut-manager' => 'b3c1b6e7', 'phabricator-main-menu-view' => '023adc14', - 'phabricator-menu-item' => '66dca903', + 'phabricator-menu-item' => 'b3c1b6e7', 'phabricator-nav-view-css' => '023adc14', - 'phabricator-notification' => '66dca903', + 'phabricator-notification' => 'b3c1b6e7', 'phabricator-notification-css' => '023adc14', 'phabricator-notification-menu-css' => '023adc14', 'phabricator-object-item-list-view-css' => '023adc14', 'phabricator-object-selector-css' => 'ec01d039', - 'phabricator-paste-file-upload' => '66dca903', - 'phabricator-prefab' => '66dca903', + 'phabricator-paste-file-upload' => 'b3c1b6e7', + 'phabricator-prefab' => 'b3c1b6e7', 'phabricator-project-tag-css' => 'e30a3fa8', 'phabricator-remarkup-css' => '023adc14', 'phabricator-shaped-request' => '310cd201', 'phabricator-side-menu-view-css' => '023adc14', 'phabricator-standard-page-view' => '023adc14', - 'phabricator-textareautils' => '66dca903', - 'phabricator-tooltip' => '66dca903', + 'phabricator-textareautils' => 'b3c1b6e7', + 'phabricator-tooltip' => 'b3c1b6e7', 'phabricator-transaction-view-css' => '023adc14', 'phabricator-zindex-css' => '023adc14', 'sprite-apps-large-css' => '023adc14', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5090f256e3..c5474942ed 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -218,6 +218,7 @@ phutil_register_library_map(array( 'ConpherenceViewController' => 'applications/conpherence/controller/ConpherenceViewController.php', 'DarkConsoleController' => 'aphront/console/DarkConsoleController.php', 'DarkConsoleCore' => 'aphront/console/DarkConsoleCore.php', + 'DarkConsoleDataController' => 'aphront/console/DarkConsoleDataController.php', 'DarkConsoleErrorLogPlugin' => 'aphront/console/plugin/DarkConsoleErrorLogPlugin.php', 'DarkConsoleErrorLogPluginAPI' => 'aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php', 'DarkConsoleEventPlugin' => 'aphront/console/plugin/DarkConsoleEventPlugin.php', @@ -1693,6 +1694,7 @@ phutil_register_library_map(array( 'ConpherenceUpdateController' => 'ConpherenceController', 'ConpherenceViewController' => 'ConpherenceController', 'DarkConsoleController' => 'PhabricatorController', + 'DarkConsoleDataController' => 'PhabricatorController', 'DarkConsoleErrorLogPlugin' => 'DarkConsolePlugin', 'DarkConsoleEventPlugin' => 'DarkConsolePlugin', 'DarkConsoleEventPluginAPI' => 'PhutilEventListener', diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php index 6d17506831..764df9f141 100644 --- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php @@ -73,7 +73,10 @@ class AphrontDefaultApplicationConfiguration 'profile/(?P[^/]+)/' => 'PhabricatorXHProfProfileController', ), - '/~/' => 'DarkConsoleController', + '/~/' => array( + '' => 'DarkConsoleController', + 'data/(?P[^/]+)/' => 'DarkConsoleDataController', + ), '/search/' => array( '' => 'PhabricatorSearchController', diff --git a/src/aphront/console/DarkConsoleCore.php b/src/aphront/console/DarkConsoleCore.php index 2a60c24bd1..90c5ec3ca8 100644 --- a/src/aphront/console/DarkConsoleCore.php +++ b/src/aphront/console/DarkConsoleCore.php @@ -5,57 +5,32 @@ */ final class DarkConsoleCore { - const PLUGIN_ERRORLOG = 'ErrorLog'; - const PLUGIN_SERVICES = 'Services'; - const PLUGIN_EVENT = 'Event'; - const PLUGIN_XHPROF = 'XHProf'; - const PLUGIN_REQUEST = 'Request'; - - public static function getPlugins() { - return array( - self::PLUGIN_ERRORLOG, - self::PLUGIN_REQUEST, - self::PLUGIN_SERVICES, - self::PLUGIN_EVENT, - self::PLUGIN_XHPROF, - ); - } - private $plugins = array(); - private $settings; - private $coredata; - - public function getPlugin($plugin_name) { - return idx($this->plugins, $plugin_name); - } + const STORAGE_VERSION = 1; public function __construct() { - foreach (self::getPlugins() as $plugin_name) { - $plugin = self::newPlugin($plugin_name); - if ($plugin->isPermanent() || !isset($disabled[$plugin_name])) { - if ($plugin->shouldStartup()) { - $plugin->didStartup(); - $plugin->setConsoleCore($this); - $this->plugins[$plugin_name] = $plugin; - } + $symbols = id(new PhutilSymbolLoader()) + ->setType('class') + ->setAncestorClass('DarkConsolePlugin') + ->selectAndLoadSymbols(); + + foreach ($symbols as $symbol) { + $plugin = newv($symbol['name'], array()); + if (!$plugin->shouldStartup()) { + continue; } + $plugin->setConsoleCore($this); + $plugin->didStartup(); + $this->plugins[$symbol['name']] = $plugin; } } - public static function newPlugin($plugin) { - $class = 'DarkConsole'.$plugin.'Plugin'; - return newv($class, array()); - } - - public function getEnabledPlugins() { + public function getPlugins() { return $this->plugins; } - public function render(AphrontRequest $request) { - - $user = $request->getUser(); - - $plugins = $this->getEnabledPlugins(); + public function getKey(AphrontRequest $request) { + $plugins = $this->getPlugins(); foreach ($plugins as $plugin) { $plugin->setRequest($request); @@ -70,128 +45,58 @@ final class DarkConsoleCore { $plugin->setData($plugin->generateData()); } - $selected = $user->getConsoleTab(); - $visible = $user->getConsoleVisible(); + $plugins = msort($plugins, 'getOrderKey'); - if (!isset($plugins[$selected])) { - $selected = head_key($plugins); - } + $key = Filesystem::readRandomCharacters(24); $tabs = array(); - foreach ($plugins as $key => $plugin) { - $tabs[$key] = array( + $data = array(); + foreach ($plugins as $plugin) { + $class = get_class($plugin); + $tabs[] = array( + 'class' => $class, 'name' => $plugin->getName(), - 'panel' => $plugin->render(), + 'color' => $plugin->getColor(), ); + $data[$class] = $plugin->getData(); } - $tabs_markup = array(); - $panel_markup = array(); - foreach ($tabs as $key => $data) { - $is_selected = ($key == $selected); - if ($is_selected) { - $style = null; - $tabclass = 'dark-console-tab-selected'; - } else { - $style = 'display: none;'; - $tabclass = null; - } + $storage = array( + 'vers' => self::STORAGE_VERSION, + 'tabs' => $tabs, + 'data' => $data, + 'user' => $request->getUser() + ? $request->getUser()->getPHID() + : null, + ); - $tabs_markup[] = javelin_tag( - 'a', - array( - 'class' => "dark-console-tab {$tabclass}", - 'sigil' => 'dark-console-tab', - 'id' => 'dark-console-tab-'.$key, - ), - (string)$data['name']); + $cache = new PhabricatorKeyValueDatabaseCache(); + $cache = new PhutilKeyValueCacheProfiler($cache); + $cache->setProfiler(PhutilServiceProfiler::getInstance()); - $panel_markup[] = javelin_render_tag( - 'div', - array( - 'class' => 'dark-console-panel dark-console-panel-'.$key, - 'style' => $style, - 'sigil' => 'dark-console-panel', - ), - (string)$data['panel']); - } - - $console = javelin_render_tag( - 'table', + $cache->setKeys( array( - 'class' => 'dark-console', - 'sigil' => 'dark-console', - 'style' => $visible ? '' : 'display: none;', + 'darkconsole:'.$key => json_encode($storage), ), - ''. - ''. - implode("\n", $tabs_markup). - ''. - ''.implode("\n", $panel_markup).''. - ''); + $ttl = (60 * 60 * 6)); - if (!empty($_COOKIE['phsid'])) { - $console = str_replace( - $_COOKIE['phsid'], - phutil_escape_html(''), - $console); - } - - if ($request->isAjax()) { - - // for ajax this HTML gets updated on the client - $request_history = null; - - } else { - - $request_table_header = - '
'; - - $rows = array(); - - $table = new AphrontTableView($rows); - $table->setHeaders( - array( - 'Sequence', - 'Type', - 'URI', - )); - $table->setColumnClasses( - array( - '', - '', - 'wide', - )); - - $request_table = $request_table_header . $table->render(); - $request_history = javelin_render_tag( - 'table', - array( - 'class' => 'dark-console dark-console-request-log', - 'sigil' => 'dark-console-request-log', - 'style' => $visible ? '' : 'display: none;', - ), - ''. - ''. - phutil_tag( - 'a', - array( - 'class' => 'dark-console-tab dark-console-tab-selected', - ), - 'Request Log'). - ''. - ''. - javelin_render_tag( - 'div', - array( - 'class' => 'dark-console-panel dark-console-panel-RequestLog', - ), - $request_table). - ''. - ''); - } - - return "\n\n\n\n".$console.$request_history."\n\n\n\n"; + return $key; } + + public function render(AphrontRequest $request) { + $user = $request->getUser(); + $visible = $user ? $user->getConsoleVisible() : true; + + return javelin_tag( + 'div', + array( + 'id' => 'darkconsole', + 'class' => 'dark-console', + 'style' => $visible ? '' : 'display: none;', + 'data-console-key' => $this->getKey($request), + ), + ''); + } + } diff --git a/src/aphront/console/DarkConsoleDataController.php b/src/aphront/console/DarkConsoleDataController.php new file mode 100644 index 0000000000..e5f1cd9e84 --- /dev/null +++ b/src/aphront/console/DarkConsoleDataController.php @@ -0,0 +1,69 @@ +key = $data['key']; + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $cache = new PhabricatorKeyValueDatabaseCache(); + $cache = new PhutilKeyValueCacheProfiler($cache); + $cache->setProfiler(PhutilServiceProfiler::getInstance()); + + $result = $cache->getKey('darkconsole:'.$this->key); + if (!$result) { + return new Aphront400Response(); + } + + $result = json_decode($result, true); + + if (!is_array($result)) { + return new Aphront400Response(); + } + + if ($result['vers'] != DarkConsoleCore::STORAGE_VERSION) { + return new Aphront400Response(); + } + + if ($result['user'] != $user->getPHID()) { + return new Aphront400Response(); + } + + $output = array(); + $output['tabs'] = $result['tabs']; + $output['panel'] = array(); + + foreach ($result['data'] as $class => $data) { + try { + $obj = newv($class, array()); + $obj->setData($data); + $obj->setRequest($request); + + $panel = $obj->renderPanel(); + + if (!empty($_COOKIE['phsid'])) { + $panel = str_replace( + $_COOKIE['phsid'], + '(session-key)', + $panel); + } + + $output['panel'][$class] = $panel; + } catch (Exception $ex) { + $output['panel'][$class] = 'error'; + } + } + + return id(new AphrontAjaxResponse())->setContent($output); + } + +} diff --git a/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php b/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php index fae5ff2cb1..98df6d113c 100644 --- a/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php @@ -7,29 +7,32 @@ final class DarkConsoleErrorLogPlugin extends DarkConsolePlugin { public function getName() { $count = count($this->getData()); - if ($count) { - return - ' '. - "Error Log ({$count})"; + return pht('Error Log (%d)', $count); } - - return 'Error Log'; + return pht('Error Log'); } + public function getOrder() { + return 0; + } + + public function getColor() { + if (count($this->getData())) { + return '#ff0000'; + } + return null; + } public function getDescription() { - return 'Shows errors and warnings.'; + return pht('Shows errors and warnings.'); } - public function generateData() { return DarkConsoleErrorLogPluginAPI::getErrors(); } - - public function render() { - + public function renderPanel() { $data = $this->getData(); $rows = array(); diff --git a/src/aphront/console/plugin/DarkConsoleEventPlugin.php b/src/aphront/console/plugin/DarkConsoleEventPlugin.php index d4bd7dcc3b..8a855bb3f8 100644 --- a/src/aphront/console/plugin/DarkConsoleEventPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleEventPlugin.php @@ -37,7 +37,7 @@ final class DarkConsoleEventPlugin extends DarkConsolePlugin { ); } - public function render() { + public function renderPanel() { $data = $this->getData(); $out = array(); diff --git a/src/aphront/console/plugin/DarkConsolePlugin.php b/src/aphront/console/plugin/DarkConsolePlugin.php index 2499cfbc7a..20a93199c0 100644 --- a/src/aphront/console/plugin/DarkConsolePlugin.php +++ b/src/aphront/console/plugin/DarkConsolePlugin.php @@ -11,12 +11,27 @@ abstract class DarkConsolePlugin { abstract public function getName(); abstract public function getDescription(); - abstract public function render(); + abstract public function renderPanel(); public function __construct() { } + public function getColor() { + return null; + } + + final public function getOrderKey() { + return sprintf( + '%09d%s', + (int)(999999999 * $this->getOrder()), + $this->getName()); + } + + public function getOrder() { + return 1.0; + } + public function setConsoleCore(DarkConsoleCore $core) { $this->core = $core; return $this; @@ -52,10 +67,6 @@ abstract class DarkConsolePlugin { return $this->getRequest()->getRequestURI(); } - public function isPermanent() { - return false; - } - public function shouldStartup() { return true; } diff --git a/src/aphront/console/plugin/DarkConsoleRequestPlugin.php b/src/aphront/console/plugin/DarkConsoleRequestPlugin.php index e252f41e8a..7a59df8493 100644 --- a/src/aphront/console/plugin/DarkConsoleRequestPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleRequestPlugin.php @@ -20,8 +20,7 @@ final class DarkConsoleRequestPlugin extends DarkConsolePlugin { ); } - public function render() { - + public function renderPanel() { $data = $this->getData(); $sections = array( diff --git a/src/aphront/console/plugin/DarkConsoleServicesPlugin.php b/src/aphront/console/plugin/DarkConsoleServicesPlugin.php index 29e7c50d21..094abea57d 100644 --- a/src/aphront/console/plugin/DarkConsoleServicesPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleServicesPlugin.php @@ -136,11 +136,16 @@ final class DarkConsoleServicesPlugin extends DarkConsolePlugin { 'start' => PhabricatorStartup::getStartTime(), 'end' => microtime(true), 'log' => $log, + 'analyzeURI' => (string)$this + ->getRequestURI() + ->alter('__analyze__', true), + 'didAnalyze' => isset($_REQUEST['__analyze__']), ); } - public function render() { + public function renderPanel() { $data = $this->getData(); + $log = $data['log']; $results = array(); @@ -149,8 +154,8 @@ final class DarkConsoleServicesPlugin extends DarkConsolePlugin { phutil_tag( 'a', array( - 'href' => $this->getRequestURI()->alter('__analyze__', true), - 'class' => isset($_REQUEST['__analyze__']) + 'href' => $data['analyzeURI'], + 'class' => $data['didAnalyze'] ? 'disabled button' : 'green button', ), diff --git a/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php b/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php index 9368cb7e66..4574056deb 100644 --- a/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php @@ -8,28 +8,40 @@ final class DarkConsoleXHProfPlugin extends DarkConsolePlugin { protected $xhprofID; public function getName() { - $run = $this->getData(); - - if ($run) { - return ' XHProf'; - } - return 'XHProf'; } + public function getColor() { + $data = $this->getData(); + if ($data['xhprofID']) { + return '#ff00ff'; + } + return null; + } + public function getDescription() { return 'Provides detailed PHP profiling information through XHProf.'; } public function generateData() { - return $this->xhprofID; + return array( + 'xhprofID' => $this->xhprofID, + 'profileURI' => (string)$this + ->getRequestURI() + ->alter('__profile__', 'page'), + ); } public function getXHProfRunID() { return $this->xhprofID; } - public function render() { + public function renderPanel() { + $data = $this->getData(); + + $run = $data['xhprofID']; + $profile_uri = $data['profileURI']; + if (!DarkConsoleXHProfPluginAPI::isProfilerAvailable()) { $href = PhabricatorEnv::getDoclink('article/Installation_Guide.html'); $install_guide = phutil_tag( @@ -49,14 +61,12 @@ final class DarkConsoleXHProfPlugin extends DarkConsolePlugin { $result = array(); - $run = $this->getXHProfRunID(); - $header = '
'. phutil_tag( 'a', array( - 'href' => $this->getRequestURI()->alter('__profile__', 'page'), + 'href' => $profile_uri, 'class' => $run ? 'disabled button' : 'green button', diff --git a/src/aphront/response/AphrontAjaxResponse.php b/src/aphront/response/AphrontAjaxResponse.php index ff2dc2a9de..0b982000f7 100644 --- a/src/aphront/response/AphrontAjaxResponse.php +++ b/src/aphront/response/AphrontAjaxResponse.php @@ -38,10 +38,10 @@ final class AphrontAjaxResponse extends AphrontResponse { $console = $this->getConsole(); if ($console) { Javelin::initBehavior( - 'dark-console-ajax', + 'dark-console', array( - 'console' => $console->render($this->getRequest()), - 'uri' => (string) $this->getRequest()->getRequestURI(), + 'uri' => (string)$this->getRequest()->getRequestURI(), + 'key' => $console->getKey($this->getRequest()), )); } diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index 2e44c4af3d..e2db6f8d34 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -164,8 +164,9 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView { Javelin::initBehavior( 'dark-console', array( - 'uri' => '/~/', - 'request_uri' => $request ? (string) $request->getRequestURI() : '/', + 'uri' => $request ? (string)$request->getRequestURI() : '?', + 'selected' => $user ? $user->getConsoleTab() : null, + 'visible' => $user ? (int)$user->getConsoleVisible() : true, )); // Change this to initBehavior when there is some behavior to initialize @@ -225,13 +226,15 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView { } protected function willSendResponse($response) { + $request = $this->getRequest(); $response = parent::willSendResponse($response); - $console = $this->getRequest()->getApplicationConfiguration()->getConsole(); + $console = $request->getApplicationConfiguration()->getConsole(); + if ($console) { $response = str_replace( '', - $console->render($this->getRequest()), + $console->render($request), $response); } diff --git a/webroot/rsrc/css/aphront/dark-console.css b/webroot/rsrc/css/aphront/dark-console.css index ceb874ab4d..5be2bcfaa4 100644 --- a/webroot/rsrc/css/aphront/dark-console.css +++ b/webroot/rsrc/css/aphront/dark-console.css @@ -3,7 +3,7 @@ */ .dark-console { - background: #555555; + background: #444444; color: #eeeeee; width: 100%; font-family: "Verdana"; @@ -11,54 +11,101 @@ position: relative; } -.dark-console a:link { - color: inherit; +.dark-console-requests, +.dark-console-tabs { + position: absolute; + overflow-y: auto; + top: 0; + left: 0; + bottom: 0; + width: 15%; + padding: 8px 0; +} + +.dark-console-requests, +.dark-console-tabs, +.dark-console-panel, +.dark-console-load { + border-left: 1px solid #111111; + box-shadow: -2px 0px 2px rgba(0, 0, 0, 0.25); +} + +.dark-console-requests { + background: #222222; } .dark-console-tabs { - width: 180px; - background: #222222; - border-right: 1px solid #888888; - padding: 2.5em 0em; + background: #333333; + left: 15%; } +.dark-console-panel, +.dark-console-load { + position: relative; + min-height: 320px; +} -a.dark-console-tab { - padding: .75em 12px; - text-align: right; - background: #444444; - position: relative; - border: 1px solid #666666; +.dark-console-panel { + margin-left: 30%; + background: #444444; +} + +.dark-console-requests a.dark-console-request, +.dark-console-tabs a.dark-console-tab { + display: block; + padding: 6px; + overflow: hidden; + background: #444444; + margin: 3px 0; + color: #cccccc; + border-color: #666666; border-width: 1px 0; - border-right-color: #888888; - margin-bottom: 2px; - display: block; - color: #cccccc; + border-style: solid; } -a.dark-console-tab-selected { - margin-right: -1px; - padding-right: 13px; - background: #555555; - border-color: #888888; - border-right-color: #555555; - color: #eeeeee; +.dark-console-requests a.dark-selected, +.dark-console-tabs a.dark-selected { + background: #0066aa; } +.dark-console-requests a.dark-console-request:hover, +.dark-console-tabs a.dark-console-tab:hover { + background: #1188cc; +} + +.dark-console-tabs a.dark-console-tab { + text-align: right; +} + +.dark-console-load { + background-image: url(/rsrc/image/darkload.gif); + background-position: center center; + background-repeat: no-repeat; + background-color: #000; + margin-left: 15%; +} .dark-console .aphront-table-view { + font-size: 11px; background: #888888; color: #eeeeee; - margin: 1em 1%; - width: 98%; + width: 100%; border-color: #333333; + margin: 8px 0; } .dark-console .aphront-table-view th { + text-shadow: none; + font-family: "Verdana"; + font-size: 11px; background: #333333; color: #ffffff; } +.dark-console .aphront-table-view td { + font-size: 11px; +} + .dark-console .aphront-table-view td.header { background: #444444; color: #ffffff; @@ -77,13 +124,8 @@ a.dark-console-tab-selected { color: #dddddd; } -.dark-console-panel-ErrorLog { - max-height: 500px; - overflow: auto; -} - -.dark-console-panel-error-details { - display: none; +.dark-console-panel-core { + padding: 12px; } .explain-sev-1 { @@ -119,15 +161,11 @@ a.dark-console-tab-selected { } .dark-console-panel-header { - background: #606060; - border-bottom: 1px solid #505050; - padding: .25em 1em .25em 0; + padding: 8px 4px 0; } .dark-console-panel-header h1 { - padding: 1em; - font-size: 12px; - font-weight: normal; + font-size: 15px; } .dark-console-panel-header .button { @@ -168,4 +206,11 @@ a.dark-console-tab-selected { height: 2px; } +.dark-console-panel-ErrorLog { + max-height: 500px; + overflow: auto; +} +.dark-console-panel-error-details { + display: none; +} diff --git a/webroot/rsrc/image/darkload.gif b/webroot/rsrc/image/darkload.gif new file mode 100644 index 0000000000000000000000000000000000000000..1b8c1f28ad188ecd1fd8f5813704bbe43633635d GIT binary patch literal 9427 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