mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-28 00:10:57 +01:00
Improve DarkConsole "Services" and "XHProf" plugins
Summary: - Services: Show summary panel of total service call costs and relative page weight. - Services: Add "Analyze Query Plans" button, which issues EXPLAIN for each query and flags problems. - XHPRof: iframe the profile. Test Plan: Used the new query plan analysis to find missing keys causing table scans, see D627. Reviewers: jungejason, tuomaspelkonen, aran CC: Differential Revision: 628
This commit is contained in:
parent
f95913ec47
commit
c33eecf438
12 changed files with 341 additions and 31 deletions
|
@ -27,7 +27,7 @@ celerity_register_resource_map(array(
|
||||||
),
|
),
|
||||||
'aphront-dark-console-css' =>
|
'aphront-dark-console-css' =>
|
||||||
array(
|
array(
|
||||||
'uri' => '/res/e7011594/rsrc/css/aphront/dark-console.css',
|
'uri' => '/res/a7d1dbf1/rsrc/css/aphront/dark-console.css',
|
||||||
'type' => 'css',
|
'type' => 'css',
|
||||||
'requires' =>
|
'requires' =>
|
||||||
array(
|
array(
|
||||||
|
|
|
@ -61,6 +61,10 @@ abstract class DarkConsolePlugin {
|
||||||
return $this->request;
|
return $this->request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRequestURI() {
|
||||||
|
return $this->getRequest()->getRequestURI();
|
||||||
|
}
|
||||||
|
|
||||||
public function isPermanent() {
|
public function isPermanent() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,22 +30,198 @@ class DarkConsoleServicesPlugin extends DarkConsolePlugin {
|
||||||
|
|
||||||
public function generateData() {
|
public function generateData() {
|
||||||
|
|
||||||
|
$log = PhutilServiceProfiler::getInstance()->getServiceCallLog();
|
||||||
|
foreach ($log as $key => $entry) {
|
||||||
|
$config = $entry['config'];
|
||||||
|
unset($log[$key]['config']);
|
||||||
|
|
||||||
|
if (empty($_REQUEST['__analyze__'])) {
|
||||||
|
$log[$key]['explain'] = array(
|
||||||
|
'sev' => 7,
|
||||||
|
'size' => null,
|
||||||
|
'reason' => 'Disabled',
|
||||||
|
);
|
||||||
|
// Query analysis is disabled for this request, so don't do any of it.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entry['type'] != 'query') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each SELECT query, go issue an EXPLAIN on it so we can flag stuff
|
||||||
|
// causing table scans, etc.
|
||||||
|
if (preg_match('/^\s*SELECT\b/i', $entry['query'])) {
|
||||||
|
$conn = new AphrontMySQLDatabaseConnection($entry['config']);
|
||||||
|
try {
|
||||||
|
$explain = queryfx_all(
|
||||||
|
$conn,
|
||||||
|
'EXPLAIN %Q',
|
||||||
|
$entry['query']);
|
||||||
|
|
||||||
|
$badness = 0;
|
||||||
|
$size = 1;
|
||||||
|
$reason = null;
|
||||||
|
|
||||||
|
foreach ($explain as $table) {
|
||||||
|
$size *= (int)$table['rows'];
|
||||||
|
|
||||||
|
switch ($table['type']) {
|
||||||
|
case 'index':
|
||||||
|
$cur_badness = 1;
|
||||||
|
$cur_reason = 'Index';
|
||||||
|
break;
|
||||||
|
case 'const':
|
||||||
|
$cur_badness = 1;
|
||||||
|
$cur_reason = 'Const';
|
||||||
|
break;
|
||||||
|
case 'eq_ref';
|
||||||
|
$cur_badness = 2;
|
||||||
|
$cur_reason = 'EqRef';
|
||||||
|
break;
|
||||||
|
case 'range':
|
||||||
|
$cur_badness = 3;
|
||||||
|
$cur_reason = 'Range';
|
||||||
|
break;
|
||||||
|
case 'ref':
|
||||||
|
$cur_badness = 3;
|
||||||
|
$cur_reason = 'Ref';
|
||||||
|
break;
|
||||||
|
case 'ALL':
|
||||||
|
if (preg_match('/Using where/', $table['Extra'])) {
|
||||||
|
if ($table['rows'] < 256 && !empty($table['possible_keys'])) {
|
||||||
|
$cur_badness = 2;
|
||||||
|
$cur_reason = 'Small Table Scan';
|
||||||
|
} else {
|
||||||
|
$cur_badness = 6;
|
||||||
|
$cur_reason = 'TABLE SCAN!';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$cur_badness = 3;
|
||||||
|
$cur_reason = 'Whole Table';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (preg_match('/No tables used/i', $table['Extra'])) {
|
||||||
|
$cur_badness = 1;
|
||||||
|
$cur_reason = 'No Tables';
|
||||||
|
} else if (preg_match('/Impossible/i', $table['Extra'])) {
|
||||||
|
$cur_badness = 1;
|
||||||
|
$cur_reason = 'Empty';
|
||||||
|
} else {
|
||||||
|
$cur_badness = 4;
|
||||||
|
$cur_reason = "Can't Analyze";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($cur_badness > $badness) {
|
||||||
|
$badness = $cur_badness;
|
||||||
|
$reason = $cur_reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$log[$key]['explain'] = array(
|
||||||
|
'sev' => $badness,
|
||||||
|
'size' => $size,
|
||||||
|
'reason' => $reason,
|
||||||
|
);
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
$log[$key]['explain'] = array(
|
||||||
|
'sev' => 5,
|
||||||
|
'size' => null,
|
||||||
|
'reason' => $ex->getMessage(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
'start' => $GLOBALS['__start__'],
|
'start' => $GLOBALS['__start__'],
|
||||||
'log' => PhutilServiceProfiler::getInstance()->getServiceCallLog(),
|
'end' => microtime(true),
|
||||||
|
'log' => $log,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render() {
|
public function render() {
|
||||||
$data = $this->getData();
|
$data = $this->getData();
|
||||||
$log = $data['log'];
|
$log = $data['log'];
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$results[] =
|
||||||
|
'<div class="dark-console-panel-header">'.
|
||||||
|
phutil_render_tag(
|
||||||
|
'a',
|
||||||
|
array(
|
||||||
|
'href' => $this->getRequestURI()->alter('__analyze__', true),
|
||||||
|
'class' => isset($_REQUEST['__analyze__'])
|
||||||
|
? 'disabled button'
|
||||||
|
: 'green button',
|
||||||
|
),
|
||||||
|
'Analyze Query Plans').
|
||||||
|
'<h1>Calls to External Services</h1>'.
|
||||||
|
'<div style="clear: both;"></div>'.
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
$page_total = $data['end'] - $data['start'];
|
||||||
|
$totals = array();
|
||||||
|
$counts = array();
|
||||||
|
|
||||||
|
foreach ($log as $row) {
|
||||||
|
$totals[$row['type']] += $row['duration'];
|
||||||
|
$counts[$row['type']]++;
|
||||||
|
}
|
||||||
|
$totals['All'] = array_sum($totals);
|
||||||
|
$counts['All'] = array_sum($counts);
|
||||||
|
|
||||||
|
$table = new AphrontTableView();
|
||||||
|
$summary = array();
|
||||||
|
foreach ($totals as $type => $total) {
|
||||||
|
$summary[] = array(
|
||||||
|
$type,
|
||||||
|
number_format($counts[$type]),
|
||||||
|
number_format((int)(1000000 * $totals[$type])).' us',
|
||||||
|
sprintf('%.1f%%', 100 * $totals[$type] / $page_total),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$summary_table = new AphrontTableView($summary);
|
||||||
|
$summary_table->setColumnClasses(
|
||||||
|
array(
|
||||||
|
'',
|
||||||
|
'n',
|
||||||
|
'n',
|
||||||
|
'wide',
|
||||||
|
));
|
||||||
|
$summary_table->setHeaders(
|
||||||
|
array(
|
||||||
|
'Type',
|
||||||
|
'Count',
|
||||||
|
'Total Cost',
|
||||||
|
'Page Weight',
|
||||||
|
));
|
||||||
|
|
||||||
|
$results[] = $summary_table->render();
|
||||||
|
|
||||||
$rows = array();
|
$rows = array();
|
||||||
foreach ($log as $row) {
|
foreach ($log as $row) {
|
||||||
|
|
||||||
|
$analysis = null;
|
||||||
|
|
||||||
switch ($row['type']) {
|
switch ($row['type']) {
|
||||||
case 'query':
|
case 'query':
|
||||||
$info = $row['query'];
|
$info = $row['query'];
|
||||||
|
$info = wordwrap($info, 128, "\n", true);
|
||||||
|
|
||||||
|
if (!empty($row['explain'])) {
|
||||||
|
$analysis = phutil_escape_html($row['explain']['reason']);
|
||||||
|
$analysis = phutil_render_tag(
|
||||||
|
'span',
|
||||||
|
array(
|
||||||
|
'class' => 'explain-sev-'.$row['explain']['sev'],
|
||||||
|
),
|
||||||
|
$analysis);
|
||||||
|
}
|
||||||
|
|
||||||
$info = phutil_escape_html($info);
|
$info = phutil_escape_html($info);
|
||||||
break;
|
break;
|
||||||
case 'connect':
|
case 'connect':
|
||||||
|
@ -70,6 +246,7 @@ class DarkConsoleServicesPlugin extends DarkConsolePlugin {
|
||||||
'+'.number_format(1000 * ($row['begin'] - $data['start'])).' ms',
|
'+'.number_format(1000 * ($row['begin'] - $data['start'])).' ms',
|
||||||
number_format(1000000 * $row['duration']).' us',
|
number_format(1000000 * $row['duration']).' us',
|
||||||
$info,
|
$info,
|
||||||
|
$analysis,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +256,8 @@ class DarkConsoleServicesPlugin extends DarkConsolePlugin {
|
||||||
null,
|
null,
|
||||||
'n',
|
'n',
|
||||||
'n',
|
'n',
|
||||||
'wide wrap',
|
'wide',
|
||||||
|
'',
|
||||||
));
|
));
|
||||||
$table->setHeaders(
|
$table->setHeaders(
|
||||||
array(
|
array(
|
||||||
|
@ -87,9 +265,12 @@ class DarkConsoleServicesPlugin extends DarkConsolePlugin {
|
||||||
'Start',
|
'Start',
|
||||||
'Duration',
|
'Duration',
|
||||||
'Details',
|
'Details',
|
||||||
|
'Analysis',
|
||||||
));
|
));
|
||||||
|
|
||||||
return $table->render();
|
$results[] = $table->render();
|
||||||
|
|
||||||
|
return implode("\n", $results);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
|
|
||||||
phutil_require_module('phabricator', 'aphront/console/plugin/base');
|
phutil_require_module('phabricator', 'aphront/console/plugin/base');
|
||||||
|
phutil_require_module('phabricator', 'storage/connection/mysql');
|
||||||
|
phutil_require_module('phabricator', 'storage/queryfx');
|
||||||
phutil_require_module('phabricator', 'view/control/table');
|
phutil_require_module('phabricator', 'view/control/table');
|
||||||
|
|
||||||
phutil_require_module('phutil', 'markup');
|
phutil_require_module('phutil', 'markup');
|
||||||
|
|
|
@ -44,36 +44,58 @@ class DarkConsoleXHProfPlugin extends DarkConsolePlugin {
|
||||||
|
|
||||||
public function render() {
|
public function render() {
|
||||||
if (!DarkConsoleXHProfPluginAPI::isProfilerAvailable()) {
|
if (!DarkConsoleXHProfPluginAPI::isProfilerAvailable()) {
|
||||||
|
$href = PhabricatorEnv::getDoclink('article/Installation_Guide.html');
|
||||||
|
$install_guide = phutil_render_tag(
|
||||||
|
'a',
|
||||||
|
array(
|
||||||
|
'href' => $href,
|
||||||
|
'class' => 'bright-link',
|
||||||
|
),
|
||||||
|
'Installation Guide');
|
||||||
return
|
return
|
||||||
'<p>The "xhprof" PHP extension is not available. Install xhprof '.
|
'<div class="dark-console-no-content">'.
|
||||||
'to enable the XHProf plugin.';
|
'The "xhprof" PHP extension is not available. Install xhprof '.
|
||||||
|
'to enable the XHProf console plugin. You can find instructions in '.
|
||||||
|
'the '.$install_guide.'.'.
|
||||||
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
|
||||||
$run = $this->getXHProfRunID();
|
$run = $this->getXHProfRunID();
|
||||||
if ($run) {
|
|
||||||
return '<a href="/xhprof/profile/'.$run.'/">View Run</a>';
|
|
||||||
} else {
|
|
||||||
$hidden = array();
|
|
||||||
$data = array('__profile__' => 'page') + $_GET;
|
|
||||||
|
|
||||||
foreach ($data as $k => $v) {
|
$header =
|
||||||
$hidden[] = phutil_render_tag(
|
'<div class="dark-console-panel-header">'.
|
||||||
'input',
|
phutil_render_tag(
|
||||||
|
'a',
|
||||||
array(
|
array(
|
||||||
'type' => 'hidden',
|
'href' => $this->getRequestURI()->alter('__profile__', 'page'),
|
||||||
'name' => $k,
|
'class' => $run
|
||||||
'value' => $v,
|
? 'disabled button'
|
||||||
));
|
: 'green button',
|
||||||
}
|
),
|
||||||
$hidden = implode("\n", $hidden);
|
'Profile Page').
|
||||||
|
'<h1>XHProf Profiler</h1>'.
|
||||||
|
'</div>';
|
||||||
|
$result[] = $header;
|
||||||
|
|
||||||
|
if ($run) {
|
||||||
return
|
$result[] =
|
||||||
'<form method="get">'.
|
'<a href="/xhprof/profile/'.$run.'/" '.
|
||||||
$hidden.
|
'class="bright-link" '.
|
||||||
'<button>Enable XHProf</button>'.
|
'style="float: right; margin: 1em 2em 0 0;'.
|
||||||
'</form>';
|
'font-weight: bold;" '.
|
||||||
|
'target="_blank">Profile Permalink</a>'.
|
||||||
|
'<iframe src="/xhprof/profile/'.$run.'/?frame=true"></iframe>';
|
||||||
|
} else {
|
||||||
|
$result[] =
|
||||||
|
'<div class="dark-console-no-content">'.
|
||||||
|
'Profiling was not enabled for this page. Use the button above '.
|
||||||
|
'to enable it.'.
|
||||||
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return implode("\n", $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
phutil_require_module('phabricator', 'aphront/console/plugin/base');
|
phutil_require_module('phabricator', 'aphront/console/plugin/base');
|
||||||
phutil_require_module('phabricator', 'aphront/console/plugin/xhprof/api');
|
phutil_require_module('phabricator', 'aphront/console/plugin/xhprof/api');
|
||||||
|
phutil_require_module('phabricator', 'infrastructure/env');
|
||||||
|
|
||||||
phutil_require_module('phutil', 'markup');
|
phutil_require_module('phutil', 'markup');
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ class PhabricatorFileListController extends PhabricatorFileController {
|
||||||
|
|
||||||
$request = $this->getRequest();
|
$request = $this->getRequest();
|
||||||
|
|
||||||
|
$author = null;
|
||||||
$author_username = $request->getStr('author');
|
$author_username = $request->getStr('author');
|
||||||
if ($author_username) {
|
if ($author_username) {
|
||||||
$author = id(new PhabricatorUser())->loadOneWhere(
|
$author = id(new PhabricatorUser())->loadOneWhere(
|
||||||
|
|
|
@ -28,6 +28,14 @@ abstract class PhabricatorXHProfController extends PhabricatorController {
|
||||||
$page->appendChild($view);
|
$page->appendChild($view);
|
||||||
|
|
||||||
$response = new AphrontWebpageResponse();
|
$response = new AphrontWebpageResponse();
|
||||||
|
|
||||||
|
if (isset($data['frame'])) {
|
||||||
|
$response->setFrameable(true);
|
||||||
|
$page->setFrameable(true);
|
||||||
|
$page->setShowChrome(false);
|
||||||
|
$page->setDisableConsole(true);
|
||||||
|
}
|
||||||
|
|
||||||
return $response->setContent($page->render());
|
return $response->setContent($page->render());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,7 @@ class PhabricatorXHProfProfileController
|
||||||
$view,
|
$view,
|
||||||
array(
|
array(
|
||||||
'title' => 'Profile',
|
'title' => 'Profile',
|
||||||
|
'frame' => $request->getBool('frame'),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -213,8 +213,9 @@ class AphrontMySQLDatabaseConnection extends AphrontDatabaseConnection {
|
||||||
$profiler = PhutilServiceProfiler::getInstance();
|
$profiler = PhutilServiceProfiler::getInstance();
|
||||||
$call_id = $profiler->beginServiceCall(
|
$call_id = $profiler->beginServiceCall(
|
||||||
array(
|
array(
|
||||||
'type' => 'query',
|
'type' => 'query',
|
||||||
'query' => $raw_query,
|
'config' => $this->configuration,
|
||||||
|
'query' => $raw_query,
|
||||||
));
|
));
|
||||||
|
|
||||||
$result = @mysql_query($raw_query, $this->connection);
|
$result = @mysql_query($raw_query, $this->connection);
|
||||||
|
|
|
@ -27,6 +27,8 @@ class PhabricatorStandardPageView extends AphrontPageView {
|
||||||
private $request;
|
private $request;
|
||||||
private $isAdminInterface;
|
private $isAdminInterface;
|
||||||
private $showChrome = true;
|
private $showChrome = true;
|
||||||
|
private $isFrameable = false;
|
||||||
|
private $disableConsole;
|
||||||
|
|
||||||
public function setIsAdminInterface($is_admin_interface) {
|
public function setIsAdminInterface($is_admin_interface) {
|
||||||
$this->isAdminInterface = $is_admin_interface;
|
$this->isAdminInterface = $is_admin_interface;
|
||||||
|
@ -51,6 +53,16 @@ class PhabricatorStandardPageView extends AphrontPageView {
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setFrameable($frameable) {
|
||||||
|
$this->isFrameable = $frameable;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDisableConsole($disable) {
|
||||||
|
$this->disableConsole = $disable;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function getApplicationName() {
|
public function getApplicationName() {
|
||||||
return $this->applicationName;
|
return $this->applicationName;
|
||||||
}
|
}
|
||||||
|
@ -103,7 +115,7 @@ class PhabricatorStandardPageView extends AphrontPageView {
|
||||||
"You must set the Request to render a PhabricatorStandardPageView.");
|
"You must set the Request to render a PhabricatorStandardPageView.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$console = $this->getRequest()->getApplicationConfiguration()->getConsole();
|
$console = $this->getConsole();
|
||||||
|
|
||||||
require_celerity_resource('phabricator-core-css');
|
require_celerity_resource('phabricator-core-css');
|
||||||
require_celerity_resource('phabricator-core-buttons-css');
|
require_celerity_resource('phabricator-core-buttons-css');
|
||||||
|
@ -133,10 +145,16 @@ class PhabricatorStandardPageView extends AphrontPageView {
|
||||||
|
|
||||||
|
|
||||||
protected function getHead() {
|
protected function getHead() {
|
||||||
|
|
||||||
|
$framebust = null;
|
||||||
|
if (!$this->isFrameable) {
|
||||||
|
$framebust = '(top != self) && top.location.replace(self.location.href);';
|
||||||
|
}
|
||||||
|
|
||||||
$response = CelerityAPI::getStaticResourceResponse();
|
$response = CelerityAPI::getStaticResourceResponse();
|
||||||
$head =
|
$head =
|
||||||
'<script type="text/javascript">'.
|
'<script type="text/javascript">'.
|
||||||
'(top != self) && top.location.replace(self.location.href);'.
|
$framebust.
|
||||||
'window.__DEV__=1;'.
|
'window.__DEV__=1;'.
|
||||||
'</script>'.
|
'</script>'.
|
||||||
$response->renderResourcesOfType('css').
|
$response->renderResourcesOfType('css').
|
||||||
|
@ -185,7 +203,7 @@ class PhabricatorStandardPageView extends AphrontPageView {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getBody() {
|
protected function getBody() {
|
||||||
$console = $this->getRequest()->getApplicationConfiguration()->getConsole();
|
$console = $this->getConsole();
|
||||||
|
|
||||||
$tabs = array();
|
$tabs = array();
|
||||||
foreach ($this->tabs as $name => $tab) {
|
foreach ($this->tabs as $name => $tab) {
|
||||||
|
@ -345,4 +363,10 @@ class PhabricatorStandardPageView extends AphrontPageView {
|
||||||
return implode(' ', $classes);
|
return implode(' ', $classes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getConsole() {
|
||||||
|
if ($this->disableConsole) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $this->getRequest()->getApplicationConfiguration()->getConsole();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,3 +87,68 @@ a.dark-console-tab-selected {
|
||||||
height: 2px;
|
height: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.explain-sev-1 {
|
||||||
|
color: #33ff33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explain-sev-2 {
|
||||||
|
color: #99ff33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explain-sev-3 {
|
||||||
|
color: #ccff33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explain-sev-4 {
|
||||||
|
color: #ffff33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explain-sev-5 {
|
||||||
|
color: #ffcc33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explain-sev-6 {
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: bold;
|
||||||
|
background: #aa0000;
|
||||||
|
padding: 0 1em;
|
||||||
|
border: 2px solid #ffff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explain-sev-7 {
|
||||||
|
color: #aaaaaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-console-panel-header {
|
||||||
|
background: #606060;
|
||||||
|
border-bottom: 1px solid #505050;
|
||||||
|
padding: .25em 1em .25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-console-panel-header h1 {
|
||||||
|
padding: 1em;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-console-panel-header .button {
|
||||||
|
margin-top: .5em;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-console-panel a.bright-link {
|
||||||
|
color: #00cfff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-console iframe {
|
||||||
|
width: 98%;
|
||||||
|
margin: .5em 1%;
|
||||||
|
height: 450px;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-console-no-content {
|
||||||
|
padding: 1.5em 2em;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue