2014-04-30 14:28:37 -07:00
|
|
|
<?php
|
|
|
|
|
|
|
|
final class PhabricatorDashboardPanelRenderingEngine extends Phobject {
|
|
|
|
|
2014-05-19 14:04:26 -07:00
|
|
|
const HEADER_MODE_NORMAL = 'normal';
|
|
|
|
const HEADER_MODE_NONE = 'none';
|
|
|
|
const HEADER_MODE_EDIT = 'edit';
|
|
|
|
|
2014-04-30 14:28:37 -07:00
|
|
|
private $panel;
|
|
|
|
private $viewer;
|
2014-04-30 14:29:14 -07:00
|
|
|
private $enableAsyncRendering;
|
2014-05-15 19:21:36 -07:00
|
|
|
private $parentPanelPHIDs;
|
2014-05-19 14:04:26 -07:00
|
|
|
private $headerMode = self::HEADER_MODE_NORMAL;
|
|
|
|
private $dashboardID;
|
|
|
|
|
|
|
|
public function setDashboardID($id) {
|
|
|
|
$this->dashboardID = $id;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getDashboardID() {
|
|
|
|
return $this->dashboardID;
|
|
|
|
}
|
2014-05-15 19:31:13 -07:00
|
|
|
|
2014-05-19 14:04:26 -07:00
|
|
|
public function setHeaderMode($header_mode) {
|
|
|
|
$this->headerMode = $header_mode;
|
2014-05-15 19:31:13 -07:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-05-19 14:04:26 -07:00
|
|
|
public function getHeaderMode() {
|
|
|
|
return $this->headerMode;
|
2014-05-15 19:31:13 -07:00
|
|
|
}
|
2014-04-30 14:29:14 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Allow the engine to render the panel via Ajax.
|
|
|
|
*/
|
|
|
|
public function setEnableAsyncRendering($enable) {
|
|
|
|
$this->enableAsyncRendering = $enable;
|
|
|
|
return $this;
|
|
|
|
}
|
2014-04-30 14:28:37 -07:00
|
|
|
|
2014-05-15 19:21:36 -07:00
|
|
|
public function setParentPanelPHIDs(array $parents) {
|
|
|
|
$this->parentPanelPHIDs = $parents;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-05-15 19:23:13 -07:00
|
|
|
public function getParentPanelPHIDs() {
|
|
|
|
return $this->parentPanelPHIDs;
|
|
|
|
}
|
|
|
|
|
2014-04-30 14:28:37 -07:00
|
|
|
public function setViewer(PhabricatorUser $viewer) {
|
|
|
|
$this->viewer = $viewer;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-05-19 14:04:26 -07:00
|
|
|
public function getViewer() {
|
|
|
|
return $this->viewer;
|
|
|
|
}
|
|
|
|
|
2014-04-30 14:28:37 -07:00
|
|
|
public function setPanel(PhabricatorDashboardPanel $panel) {
|
|
|
|
$this->panel = $panel;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-05-19 14:04:26 -07:00
|
|
|
public function getPanel() {
|
|
|
|
return $this->panel;
|
|
|
|
}
|
|
|
|
|
2014-04-30 14:28:37 -07:00
|
|
|
public function renderPanel() {
|
2014-05-19 14:04:26 -07:00
|
|
|
$panel = $this->getPanel();
|
|
|
|
$viewer = $this->getViewer();
|
2014-04-30 14:28:37 -07:00
|
|
|
|
|
|
|
if (!$panel) {
|
|
|
|
return $this->renderErrorPanel(
|
|
|
|
pht('Missing Panel'),
|
|
|
|
pht('This panel does not exist.'));
|
|
|
|
}
|
|
|
|
|
|
|
|
$panel_type = $panel->getImplementation();
|
|
|
|
if (!$panel_type) {
|
|
|
|
return $this->renderErrorPanel(
|
|
|
|
$panel->getName(),
|
|
|
|
pht(
|
|
|
|
'This panel has type "%s", but that panel type is not known to '.
|
|
|
|
'Phabricator.',
|
|
|
|
$panel->getPanelType()));
|
|
|
|
}
|
|
|
|
|
2014-05-15 19:21:36 -07:00
|
|
|
try {
|
|
|
|
$this->detectRenderingCycle($panel);
|
|
|
|
|
|
|
|
if ($this->enableAsyncRendering) {
|
|
|
|
if ($panel_type->shouldRenderAsync()) {
|
2014-05-19 14:04:26 -07:00
|
|
|
return $this->renderAsyncPanel();
|
2014-05-15 19:21:36 -07:00
|
|
|
}
|
2014-04-30 14:29:14 -07:00
|
|
|
}
|
|
|
|
|
2014-05-19 14:04:26 -07:00
|
|
|
return $this->renderNormalPanel($viewer, $panel, $this);
|
2014-05-07 14:56:30 -07:00
|
|
|
} catch (Exception $ex) {
|
|
|
|
return $this->renderErrorPanel(
|
|
|
|
$panel->getName(),
|
|
|
|
pht(
|
|
|
|
'%s: %s',
|
|
|
|
phutil_tag('strong', array(), get_class($ex)),
|
|
|
|
$ex->getMessage()));
|
|
|
|
}
|
2014-04-30 14:28:37 -07:00
|
|
|
}
|
|
|
|
|
2014-05-19 14:04:26 -07:00
|
|
|
private function renderNormalPanel() {
|
|
|
|
$panel = $this->getPanel();
|
|
|
|
$panel_type = $panel->getImplementation();
|
|
|
|
|
|
|
|
$content = $panel_type->renderPanelContent(
|
|
|
|
$this->getViewer(),
|
|
|
|
$panel,
|
|
|
|
$this);
|
|
|
|
$header = $this->renderPanelHeader();
|
|
|
|
|
|
|
|
return $this->renderPanelDiv(
|
|
|
|
$content,
|
|
|
|
$header);
|
2014-04-30 14:28:37 -07:00
|
|
|
}
|
|
|
|
|
2014-05-19 14:04:26 -07:00
|
|
|
|
|
|
|
private function renderAsyncPanel() {
|
|
|
|
$panel = $this->getPanel();
|
|
|
|
|
2014-04-30 14:29:14 -07:00
|
|
|
$panel_id = celerity_generate_unique_node_id();
|
2014-05-19 14:04:26 -07:00
|
|
|
$dashboard_id = $this->getDashboardID();
|
2014-04-30 14:29:14 -07:00
|
|
|
|
|
|
|
Javelin::initBehavior(
|
|
|
|
'dashboard-async-panel',
|
|
|
|
array(
|
|
|
|
'panelID' => $panel_id,
|
2014-05-15 19:23:13 -07:00
|
|
|
'parentPanelPHIDs' => $this->getParentPanelPHIDs(),
|
2014-05-19 14:04:26 -07:00
|
|
|
'headerMode' => $this->getHeaderMode(),
|
|
|
|
'dashboardID' => $dashboard_id,
|
2014-04-30 14:29:14 -07:00
|
|
|
'uri' => '/dashboard/panel/render/'.$panel->getID().'/',
|
|
|
|
));
|
|
|
|
|
2014-05-19 14:04:26 -07:00
|
|
|
$header = $this->renderPanelHeader();
|
|
|
|
$content = id(new PHUIPropertyListView())
|
|
|
|
->addTextContent(pht('Loading...'));
|
|
|
|
|
|
|
|
return $this->renderPanelDiv(
|
|
|
|
$content,
|
|
|
|
$header,
|
|
|
|
$panel_id);
|
|
|
|
}
|
|
|
|
|
|
|
|
private function renderErrorPanel($title, $body) {
|
|
|
|
switch ($this->getHeaderMode()) {
|
|
|
|
case self::HEADER_MODE_NONE:
|
|
|
|
$header = null;
|
|
|
|
break;
|
|
|
|
case self::HEADER_MODE_EDIT:
|
2014-06-24 09:39:32 -07:00
|
|
|
$header = id(new PHUIActionHeaderView())
|
2014-05-19 14:04:26 -07:00
|
|
|
->setHeaderTitle($title)
|
2014-06-24 09:39:32 -07:00
|
|
|
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE);
|
2014-05-19 14:04:26 -07:00
|
|
|
$header = $this->addPanelHeaderActions($header);
|
|
|
|
break;
|
|
|
|
case self::HEADER_MODE_NORMAL:
|
|
|
|
default:
|
2014-06-24 09:39:32 -07:00
|
|
|
$header = id(new PHUIActionHeaderView())
|
2014-05-19 14:04:26 -07:00
|
|
|
->setHeaderTitle($title)
|
2014-06-24 09:39:32 -07:00
|
|
|
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE);
|
2014-05-19 14:04:26 -07:00
|
|
|
break;
|
|
|
|
}
|
2014-06-12 07:11:00 -07:00
|
|
|
$icon = id(new PHUIIconView())
|
|
|
|
->setIconFont('fa-warning red msr');
|
|
|
|
$content = id(new PHUIBoxView())
|
|
|
|
->addClass('dashboard-box')
|
|
|
|
->appendChild($icon)
|
|
|
|
->appendChild($body);
|
2014-05-19 14:04:26 -07:00
|
|
|
return $this->renderPanelDiv(
|
2014-06-12 07:11:00 -07:00
|
|
|
$content,
|
2014-05-19 14:04:26 -07:00
|
|
|
$header);
|
|
|
|
}
|
|
|
|
|
|
|
|
private function renderPanelDiv(
|
|
|
|
$content,
|
|
|
|
$header = null,
|
|
|
|
$id = null) {
|
2014-05-23 21:48:15 -07:00
|
|
|
require_celerity_resource('phabricator-dashboard-css');
|
2014-05-19 14:04:26 -07:00
|
|
|
|
|
|
|
$panel = $this->getPanel();
|
|
|
|
if (!$id) {
|
|
|
|
$id = celerity_generate_unique_node_id();
|
|
|
|
}
|
|
|
|
return javelin_tag(
|
|
|
|
'div',
|
|
|
|
array(
|
|
|
|
'id' => $id,
|
|
|
|
'sigil' => 'dashboard-panel',
|
|
|
|
'meta' => array(
|
2014-10-08 00:01:04 +11:00
|
|
|
'objectPHID' => $panel->getPHID(),
|
|
|
|
),
|
|
|
|
'class' => 'dashboard-panel',
|
|
|
|
),
|
2014-05-19 14:04:26 -07:00
|
|
|
array(
|
|
|
|
$header,
|
2014-10-08 00:01:04 +11:00
|
|
|
$content,
|
|
|
|
));
|
2014-05-19 14:04:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private function renderPanelHeader() {
|
|
|
|
|
|
|
|
$panel = $this->getPanel();
|
|
|
|
switch ($this->getHeaderMode()) {
|
|
|
|
case self::HEADER_MODE_NONE:
|
|
|
|
$header = null;
|
|
|
|
break;
|
|
|
|
case self::HEADER_MODE_EDIT:
|
2014-06-24 09:39:32 -07:00
|
|
|
$header = id(new PHUIActionHeaderView())
|
2014-05-19 14:04:26 -07:00
|
|
|
->setHeaderTitle($panel->getName())
|
2014-06-24 09:39:32 -07:00
|
|
|
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE);
|
2014-05-19 14:04:26 -07:00
|
|
|
$header = $this->addPanelHeaderActions($header);
|
|
|
|
break;
|
|
|
|
case self::HEADER_MODE_NORMAL:
|
|
|
|
default:
|
2014-06-24 09:39:32 -07:00
|
|
|
$header = id(new PHUIActionHeaderView())
|
2014-05-19 14:04:26 -07:00
|
|
|
->setHeaderTitle($panel->getName())
|
2014-06-24 09:39:32 -07:00
|
|
|
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE);
|
2014-12-18 08:40:50 -08:00
|
|
|
$panel_type = $panel->getImplementation();
|
|
|
|
$header = $panel_type->adjustPanelHeader(
|
|
|
|
$this->getViewer(),
|
|
|
|
$panel,
|
|
|
|
$this,
|
|
|
|
$header);
|
2014-05-19 14:04:26 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
return $header;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function addPanelHeaderActions(
|
2014-06-24 09:39:32 -07:00
|
|
|
PHUIActionHeaderView $header) {
|
2014-05-19 14:04:26 -07:00
|
|
|
$panel = $this->getPanel();
|
|
|
|
|
|
|
|
$dashboard_id = $this->getDashboardID();
|
|
|
|
$edit_uri = id(new PhutilURI(
|
|
|
|
'/dashboard/panel/edit/'.$panel->getID().'/'));
|
|
|
|
if ($dashboard_id) {
|
|
|
|
$edit_uri->setQueryParam('dashboardID', $dashboard_id);
|
|
|
|
}
|
|
|
|
$action_edit = id(new PHUIIconView())
|
2014-05-21 10:18:43 -07:00
|
|
|
->setIconFont('fa-pencil')
|
2014-06-12 13:22:12 -07:00
|
|
|
->setWorkflow(true)
|
2015-05-20 07:06:07 +10:00
|
|
|
->setHref((string)$edit_uri);
|
2014-05-19 14:04:26 -07:00
|
|
|
$header->addAction($action_edit);
|
|
|
|
|
|
|
|
if ($dashboard_id) {
|
|
|
|
$uri = id(new PhutilURI(
|
|
|
|
'/dashboard/removepanel/'.$dashboard_id.'/'))
|
|
|
|
->setQueryParam('panelPHID', $panel->getPHID());
|
|
|
|
$action_remove = id(new PHUIIconView())
|
2014-05-21 10:18:43 -07:00
|
|
|
->setIconFont('fa-trash-o')
|
2015-05-20 07:06:07 +10:00
|
|
|
->setHref((string)$uri)
|
2014-05-19 14:04:26 -07:00
|
|
|
->setWorkflow(true);
|
|
|
|
$header->addAction($action_remove);
|
2014-05-15 19:31:13 -07:00
|
|
|
}
|
2014-05-19 14:04:26 -07:00
|
|
|
return $header;
|
2014-04-30 14:29:14 -07:00
|
|
|
}
|
|
|
|
|
2014-12-18 08:40:50 -08:00
|
|
|
|
2014-05-15 19:21:36 -07:00
|
|
|
/**
|
|
|
|
* Detect graph cycles in panels, and deeply nested panels.
|
|
|
|
*
|
|
|
|
* This method throws if the current rendering stack is too deep or contains
|
|
|
|
* a cycle. This can happen if you embed layout panels inside each other,
|
|
|
|
* build a big stack of panels, or embed a panel in remarkup inside another
|
|
|
|
* panel. Generally, all of this stuff is ridiculous and we just want to
|
|
|
|
* shut it down.
|
|
|
|
*
|
|
|
|
* @param PhabricatorDashboardPanel Panel being rendered.
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
private function detectRenderingCycle(PhabricatorDashboardPanel $panel) {
|
|
|
|
if ($this->parentPanelPHIDs === null) {
|
|
|
|
throw new Exception(
|
|
|
|
pht(
|
|
|
|
'You must call setParentPanelPHIDs() before rendering panels.'));
|
|
|
|
}
|
|
|
|
|
|
|
|
$max_depth = 4;
|
|
|
|
if (count($this->parentPanelPHIDs) >= $max_depth) {
|
|
|
|
throw new Exception(
|
|
|
|
pht(
|
|
|
|
'To render more than %s levels of panels nested inside other '.
|
|
|
|
'panels, purchase a subscription to Phabricator Gold.',
|
|
|
|
new PhutilNumber($max_depth)));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (in_array($panel->getPHID(), $this->parentPanelPHIDs)) {
|
|
|
|
throw new Exception(
|
|
|
|
pht(
|
|
|
|
'You awake in a twisting maze of mirrors, all alike. '.
|
|
|
|
'You are likely to be eaten by a graph cycle. '.
|
|
|
|
'Should you escape alive, you resolve to be more careful about '.
|
|
|
|
'putting dashboard panels inside themselves.'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-04-30 14:28:37 -07:00
|
|
|
}
|