Path: blob/master/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php
12242 views
<?php12final class PhabricatorDashboardPanelRenderingEngine extends Phobject {34const HEADER_MODE_NORMAL = 'normal';5const HEADER_MODE_NONE = 'none';6const HEADER_MODE_EDIT = 'edit';78private $panel;9private $panelPHID;10private $viewer;11private $enableAsyncRendering;12private $parentPanelPHIDs;13private $headerMode = self::HEADER_MODE_NORMAL;14private $movable;15private $panelHandle;16private $editMode;17private $contextObject;18private $panelKey;1920public function setContextObject($object) {21$this->contextObject = $object;22return $this;23}2425public function getContextObject() {26return $this->contextObject;27}2829public function setPanelKey($panel_key) {30$this->panelKey = $panel_key;31return $this;32}3334public function getPanelKey() {35return $this->panelKey;36}3738public function setHeaderMode($header_mode) {39$this->headerMode = $header_mode;40return $this;41}4243public function getHeaderMode() {44return $this->headerMode;45}4647public function setPanelHandle(PhabricatorObjectHandle $panel_handle) {48$this->panelHandle = $panel_handle;49return $this;50}5152public function getPanelHandle() {53return $this->panelHandle;54}5556public function isEditMode() {57return $this->editMode;58}5960public function setEditMode($mode) {61$this->editMode = $mode;62return $this;63}6465/**66* Allow the engine to render the panel via Ajax.67*/68public function setEnableAsyncRendering($enable) {69$this->enableAsyncRendering = $enable;70return $this;71}7273public function setParentPanelPHIDs(array $parents) {74$this->parentPanelPHIDs = $parents;75return $this;76}7778public function getParentPanelPHIDs() {79return $this->parentPanelPHIDs;80}8182public function setViewer(PhabricatorUser $viewer) {83$this->viewer = $viewer;84return $this;85}8687public function getViewer() {88return $this->viewer;89}9091public function setPanel(PhabricatorDashboardPanel $panel) {92$this->panel = $panel;93return $this;94}9596public function setMovable($movable) {97$this->movable = $movable;98return $this;99}100101public function getMovable() {102return $this->movable;103}104105public function getPanel() {106return $this->panel;107}108109public function setPanelPHID($panel_phid) {110$this->panelPHID = $panel_phid;111return $this;112}113114public function getPanelPHID() {115return $this->panelPHID;116}117118public function renderPanel() {119$panel = $this->getPanel();120121if (!$panel) {122$handle = $this->getPanelHandle();123if ($handle->getPolicyFiltered()) {124return $this->renderErrorPanel(125pht('Restricted Panel'),126pht(127'You do not have permission to see this panel.'));128} else {129return $this->renderErrorPanel(130pht('Invalid Panel'),131pht(132'This panel is invalid or does not exist. It may have been '.133'deleted.'));134}135}136137$panel_type = $panel->getImplementation();138if (!$panel_type) {139return $this->renderErrorPanel(140$panel->getName(),141pht(142'This panel has type "%s", but that panel type is unknown.',143$panel->getPanelType()));144}145146try {147$this->detectRenderingCycle($panel);148149if ($this->enableAsyncRendering) {150if ($panel_type->shouldRenderAsync()) {151return $this->renderAsyncPanel();152}153}154155return $this->renderNormalPanel();156} catch (Exception $ex) {157return $this->renderErrorPanel(158$panel->getName(),159pht(160'%s: %s',161phutil_tag('strong', array(), get_class($ex)),162$ex->getMessage()));163}164}165166private function renderNormalPanel() {167$panel = $this->getPanel();168$panel_type = $panel->getImplementation();169170$content = $panel_type->renderPanelContent(171$this->getViewer(),172$panel,173$this);174$header = $this->renderPanelHeader();175176return $this->renderPanelDiv(177$content,178$header);179}180181182private function renderAsyncPanel() {183$context_phid = $this->getContextPHID();184$panel = $this->getPanel();185186$panel_id = celerity_generate_unique_node_id();187188Javelin::initBehavior(189'dashboard-async-panel',190array(191'panelID' => $panel_id,192'parentPanelPHIDs' => $this->getParentPanelPHIDs(),193'headerMode' => $this->getHeaderMode(),194'contextPHID' => $context_phid,195'panelKey' => $this->getPanelKey(),196'movable' => $this->getMovable(),197'uri' => '/dashboard/panel/render/'.$panel->getID().'/',198));199200$header = $this->renderPanelHeader();201$content = id(new PHUIPropertyListView())202->addTextContent(pht('Loading...'));203204return $this->renderPanelDiv(205$content,206$header,207$panel_id);208}209210private function renderErrorPanel($title, $body) {211switch ($this->getHeaderMode()) {212case self::HEADER_MODE_NONE:213$header = null;214break;215case self::HEADER_MODE_EDIT:216$header = id(new PHUIHeaderView())217->setHeader($title);218$header = $this->addPanelHeaderActions($header);219break;220case self::HEADER_MODE_NORMAL:221default:222$header = id(new PHUIHeaderView())223->setHeader($title);224break;225}226227$icon = id(new PHUIIconView())228->setIcon('fa-warning red msr');229230$content = id(new PHUIBoxView())231->addClass('dashboard-box')232->addMargin(PHUI::MARGIN_LARGE)233->appendChild($icon)234->appendChild($body);235236return $this->renderPanelDiv(237$content,238$header);239}240241private function renderPanelDiv(242$content,243$header = null,244$id = null) {245require_celerity_resource('phabricator-dashboard-css');246247$panel = $this->getPanel();248if (!$id) {249$id = celerity_generate_unique_node_id();250}251252$box = new PHUIObjectBoxView();253254$interface = 'PhabricatorApplicationSearchResultView';255if ($content instanceof $interface) {256if ($content->getObjectList()) {257$box->setObjectList($content->getObjectList());258}259if ($content->getTable()) {260$box->setTable($content->getTable());261}262if ($content->getContent()) {263$box->appendChild($content->getContent());264}265} else {266$box->appendChild($content);267}268269$box270->setHeader($header)271->setID($id)272->addClass('dashboard-box')273->addSigil('dashboard-panel');274275if ($this->getMovable()) {276$box->addSigil('panel-movable');277}278279if ($panel) {280$box->setMetadata(281array(282'panelKey' => $this->getPanelKey(),283));284}285286return $box;287}288289290private function renderPanelHeader() {291292$panel = $this->getPanel();293switch ($this->getHeaderMode()) {294case self::HEADER_MODE_NONE:295$header = null;296break;297case self::HEADER_MODE_EDIT:298// In edit mode, include the panel monogram to make managing boards299// a little easier.300$header_text = pht('%s %s', $panel->getMonogram(), $panel->getName());301$header = id(new PHUIHeaderView())302->setHeader($header_text);303$header = $this->addPanelHeaderActions($header);304break;305case self::HEADER_MODE_NORMAL:306default:307$header = id(new PHUIHeaderView())308->setHeader($panel->getName());309$panel_type = $panel->getImplementation();310$header = $panel_type->adjustPanelHeader(311$this->getViewer(),312$panel,313$this,314$header);315break;316}317return $header;318}319320private function addPanelHeaderActions(321PHUIHeaderView $header) {322323$viewer = $this->getViewer();324$panel = $this->getPanel();325$context_phid = $this->getContextPHID();326327$actions = array();328329if ($panel) {330try {331$panel_actions = $panel->newHeaderEditActions(332$viewer,333$context_phid);334} catch (Exception $ex) {335$error_action = id(new PhabricatorActionView())336->setIcon('fa-exclamation-triangle red')337->setName(pht('<Rendering Exception>'));338$panel_actions[] = $error_action;339}340341if ($panel_actions) {342foreach ($panel_actions as $panel_action) {343$actions[] = $panel_action;344}345$actions[] = id(new PhabricatorActionView())346->setType(PhabricatorActionView::TYPE_DIVIDER);347}348349$panel_id = $panel->getID();350351$edit_uri = "/dashboard/panel/edit/{$panel_id}/";352$params = array(353'contextPHID' => $context_phid,354);355$edit_uri = new PhutilURI($edit_uri, $params);356357$actions[] = id(new PhabricatorActionView())358->setIcon('fa-pencil')359->setName(pht('Edit Panel'))360->setHref($edit_uri);361362$actions[] = id(new PhabricatorActionView())363->setIcon('fa-window-maximize')364->setName(pht('View Panel Details'))365->setHref($panel->getURI());366}367368if ($context_phid) {369$panel_phid = $this->getPanelPHID();370371$remove_uri = urisprintf('/dashboard/adjust/remove/');372$params = array(373'contextPHID' => $context_phid,374'panelKey' => $this->getPanelKey(),375);376$remove_uri = new PhutilURI($remove_uri, $params);377378$actions[] = id(new PhabricatorActionView())379->setIcon('fa-times')380->setHref($remove_uri)381->setName(pht('Remove Panel'))382->setWorkflow(true);383}384385$dropdown_menu = id(new PhabricatorActionListView())386->setViewer($viewer);387388foreach ($actions as $action) {389$dropdown_menu->addAction($action);390}391392$action_menu = id(new PHUIButtonView())393->setTag('a')394->setIcon('fa-cog')395->setText(pht('Manage Panel'))396->setDropdownMenu($dropdown_menu);397398$header->addActionLink($action_menu);399400return $header;401}402403404/**405* Detect graph cycles in panels, and deeply nested panels.406*407* This method throws if the current rendering stack is too deep or contains408* a cycle. This can happen if you embed layout panels inside each other,409* build a big stack of panels, or embed a panel in remarkup inside another410* panel. Generally, all of this stuff is ridiculous and we just want to411* shut it down.412*413* @param PhabricatorDashboardPanel Panel being rendered.414* @return void415*/416private function detectRenderingCycle(PhabricatorDashboardPanel $panel) {417if ($this->parentPanelPHIDs === null) {418throw new PhutilInvalidStateException('setParentPanelPHIDs');419}420421$max_depth = 4;422if (count($this->parentPanelPHIDs) >= $max_depth) {423throw new Exception(424pht(425'To render more than %s levels of panels nested inside other '.426'panels, purchase a subscription to %s Gold.',427new PhutilNumber($max_depth),428PlatformSymbols::getPlatformServerName()));429}430431if (in_array($panel->getPHID(), $this->parentPanelPHIDs)) {432throw new Exception(433pht(434'You awake in a twisting maze of mirrors, all alike. '.435'You are likely to be eaten by a graph cycle. '.436'Should you escape alive, you resolve to be more careful about '.437'putting dashboard panels inside themselves.'));438}439}440441private function getContextPHID() {442$context = $this->getContextObject();443444if ($context) {445return $context->getPHID();446}447448return null;449}450451}452453454