diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php index 82d659a7e1..38df97d926 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php @@ -1,372 +1,391 @@ dashboardID = $id; return $this; } public function getDashboardID() { return $this->dashboardID; } public function setHeaderMode($header_mode) { $this->headerMode = $header_mode; return $this; } public function getHeaderMode() { return $this->headerMode; } + public function setPanelHandle(PhabricatorObjectHandle $panel_handle) { + $this->panelHandle = $panel_handle; + return $this; + } + + public function getPanelHandle() { + return $this->panelHandle; + } + /** * Allow the engine to render the panel via Ajax. */ public function setEnableAsyncRendering($enable) { $this->enableAsyncRendering = $enable; return $this; } public function setParentPanelPHIDs(array $parents) { $this->parentPanelPHIDs = $parents; return $this; } public function getParentPanelPHIDs() { return $this->parentPanelPHIDs; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setPanel(PhabricatorDashboardPanel $panel) { $this->panel = $panel; return $this; } public function setMovable($movable) { $this->movable = $movable; return $this; } public function getMovable() { return $this->movable; } public function getPanel() { return $this->panel; } public function setPanelPHID($panel_phid) { $this->panelPHID = $panel_phid; return $this; } public function getPanelPHID() { return $this->panelPHID; } public function renderPanel() { $panel = $this->getPanel(); if (!$panel) { - return $this->renderErrorPanel( - pht('Missing or Restricted Panel'), - pht( - 'This panel does not exist, or you do not have permission '. - 'to see it.')); + $handle = $this->getPanelHandle(); + if ($handle->getPolicyFiltered()) { + return $this->renderErrorPanel( + pht('Restricted Panel'), + pht( + 'You do not have permission to see this panel.')); + } else { + return $this->renderErrorPanel( + pht('Invalid Panel'), + pht( + 'This panel is invalid or does not exist. It may have been '. + 'deleted.')); + } } $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())); } try { $this->detectRenderingCycle($panel); if ($this->enableAsyncRendering) { if ($panel_type->shouldRenderAsync()) { return $this->renderAsyncPanel(); } } return $this->renderNormalPanel(); } catch (Exception $ex) { return $this->renderErrorPanel( $panel->getName(), pht( '%s: %s', phutil_tag('strong', array(), get_class($ex)), $ex->getMessage())); } } 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); } private function renderAsyncPanel() { $panel = $this->getPanel(); $panel_id = celerity_generate_unique_node_id(); $dashboard_id = $this->getDashboardID(); Javelin::initBehavior( 'dashboard-async-panel', array( 'panelID' => $panel_id, 'parentPanelPHIDs' => $this->getParentPanelPHIDs(), 'headerMode' => $this->getHeaderMode(), 'dashboardID' => $dashboard_id, 'uri' => '/dashboard/panel/render/'.$panel->getID().'/', )); $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: $header = id(new PHUIHeaderView()) ->setHeader($title); $header = $this->addPanelHeaderActions($header); break; case self::HEADER_MODE_NORMAL: default: $header = id(new PHUIHeaderView()) ->setHeader($title); break; } + $icon = id(new PHUIIconView()) ->setIcon('fa-warning red msr'); $content = id(new PHUIBoxView()) ->addClass('dashboard-box') - ->addMargin(PHUI::MARGIN_MEDIUM) + ->addMargin(PHUI::MARGIN_LARGE) ->appendChild($icon) ->appendChild($body); return $this->renderPanelDiv( $content, $header); } private function renderPanelDiv( $content, $header = null, $id = null) { require_celerity_resource('phabricator-dashboard-css'); $panel = $this->getPanel(); if (!$id) { $id = celerity_generate_unique_node_id(); } $box = new PHUIObjectBoxView(); $interface = 'PhabricatorApplicationSearchResultView'; if ($content instanceof $interface) { if ($content->getObjectList()) { $box->setObjectList($content->getObjectList()); } if ($content->getTable()) { $box->setTable($content->getTable()); } if ($content->getContent()) { $box->appendChild($content->getContent()); } } else { $box->appendChild($content); } $box ->setHeader($header) ->setID($id) ->addClass('dashboard-box') ->addSigil('dashboard-panel'); if ($this->getMovable()) { $box->addSigil('panel-movable'); } if ($panel) { $box->setMetadata( array( 'objectPHID' => $panel->getPHID(), )); } return phutil_tag_div('dashboard-pane', $box); } private function renderPanelHeader() { $panel = $this->getPanel(); switch ($this->getHeaderMode()) { case self::HEADER_MODE_NONE: $header = null; break; case self::HEADER_MODE_EDIT: $header = id(new PHUIHeaderView()) ->setHeader($panel->getName()); $header = $this->addPanelHeaderActions($header); break; case self::HEADER_MODE_NORMAL: default: $header = id(new PHUIHeaderView()) ->setHeader($panel->getName()); $panel_type = $panel->getImplementation(); $header = $panel_type->adjustPanelHeader( $this->getViewer(), $panel, $this, $header); break; } return $header; } private function addPanelHeaderActions( PHUIHeaderView $header) { $viewer = $this->getViewer(); $panel = $this->getPanel(); $dashboard_id = $this->getDashboardID(); $actions = array(); if ($panel) { $panel_id = $panel->getID(); $edit_uri = "/dashboard/panel/edit/{$panel_id}/"; $edit_uri = new PhutilURI($edit_uri); if ($dashboard_id) { $edit_uri->replaceQueryParam('dashboardID', $dashboard_id); } $actions[] = id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Panel')) ->setHref((string)$edit_uri); } if ($dashboard_id) { $panel_phid = $this->getPanelPHID(); $remove_uri = "/dashboard/removepanel/{$dashboard_id}/"; $remove_uri = id(new PhutilURI($remove_uri)) ->replaceQueryParam('panelPHID', $panel_phid); $actions[] = id(new PhabricatorActionView()) ->setIcon('fa-times') ->setHref((string)$remove_uri) ->setName(pht('Remove Panel')) ->setWorkflow(true); } $dropdown_menu = id(new PhabricatorActionListView()) ->setViewer($viewer); foreach ($actions as $action) { $dropdown_menu->addAction($action); } $action_menu = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-cog') ->setText(pht('Manage Panel')) ->setDropdownMenu($dropdown_menu); $header->addActionLink($action_menu); return $header; } /** * 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 PhutilInvalidStateException('setParentPanelPHIDs'); } $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.')); } } } diff --git a/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php index e8fd0ad923..fb9981cb59 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php @@ -1,147 +1,156 @@ viewer = $viewer; return $this; } public function setDashboard(PhabricatorDashboard $dashboard) { $this->dashboard = $dashboard; return $this; } public function setArrangeMode($mode) { $this->arrangeMode = $mode; return $this; } public function renderDashboard() { require_celerity_resource('phabricator-dashboard-css'); $dashboard = $this->dashboard; $viewer = $this->viewer; $layout_config = $dashboard->getLayoutConfigObject(); $panel_grid_locations = $layout_config->getPanelLocations(); $panels = mpull($dashboard->getPanels(), null, 'getPHID'); $dashboard_id = celerity_generate_unique_node_id(); $result = id(new AphrontMultiColumnView()) ->setID($dashboard_id) ->setFluidLayout(true) ->setGutter(AphrontMultiColumnView::GUTTER_LARGE); if ($this->arrangeMode) { $h_mode = PhabricatorDashboardPanelRenderingEngine::HEADER_MODE_EDIT; } else { $h_mode = PhabricatorDashboardPanelRenderingEngine::HEADER_MODE_NORMAL; } + $panel_phids = array(); + foreach ($panel_grid_locations as $panel_column_locations) { + foreach ($panel_column_locations as $panel_phid) { + $panel_phids[] = $panel_phid; + } + } + $handles = $viewer->loadHandles($panel_phids); + foreach ($panel_grid_locations as $column => $panel_column_locations) { $panel_phids = $panel_column_locations; // TODO: This list may contain duplicates when the dashboard itself // does not? Perhaps this is related to T10612. For now, just unique // the list before moving on. $panel_phids = array_unique($panel_phids); $column_result = array(); foreach ($panel_phids as $panel_phid) { $panel_engine = id(new PhabricatorDashboardPanelRenderingEngine()) ->setViewer($viewer) ->setDashboardID($dashboard->getID()) ->setEnableAsyncRendering(true) ->setPanelPHID($panel_phid) ->setParentPanelPHIDs(array()) - ->setHeaderMode($h_mode); + ->setHeaderMode($h_mode) + ->setPanelHandle($handles[$panel_phid]); $panel = idx($panels, $panel_phid); if ($panel) { $panel_engine->setPanel($panel); } $column_result[] = $panel_engine->renderPanel(); } $column_class = $layout_config->getColumnClass( $column, $this->arrangeMode); if ($this->arrangeMode) { $column_result[] = $this->renderAddPanelPlaceHolder($column); $column_result[] = $this->renderAddPanelUI($column); } $result->addColumn( $column_result, $column_class, $sigil = 'dashboard-column', $metadata = array('columnID' => $column)); } if ($this->arrangeMode) { Javelin::initBehavior( 'dashboard-move-panels', array( 'dashboardID' => $dashboard_id, 'moveURI' => '/dashboard/movepanel/'.$dashboard->getID().'/', )); } $view = id(new PHUIBoxView()) ->addClass('dashboard-view') ->appendChild($result); return $view; } private function renderAddPanelPlaceHolder($column) { $dashboard = $this->dashboard; $panels = $dashboard->getPanels(); return javelin_tag( 'span', array( 'sigil' => 'workflow', 'class' => 'drag-ghost dashboard-panel-placeholder', ), pht('This column does not have any panels yet.')); } private function renderAddPanelUI($column) { $dashboard_id = $this->dashboard->getID(); $create_uri = id(new PhutilURI('/dashboard/panel/edit/')) ->replaceQueryParam('dashboardID', $dashboard_id) ->replaceQueryParam('columnID', $column); $add_uri = id(new PhutilURI('/dashboard/addpanel/'.$dashboard_id.'/')) ->replaceQueryParam('columnID', $column); $create_button = id(new PHUIButtonView()) ->setTag('a') ->setHref($create_uri) ->setWorkflow(true) ->setText(pht('Create Panel')) ->addClass(PHUI::MARGIN_MEDIUM); $add_button = id(new PHUIButtonView()) ->setTag('a') ->setHref($add_uri) ->setWorkflow(true) ->setText(pht('Add Existing Panel')) ->addClass(PHUI::MARGIN_MEDIUM); return phutil_tag( 'div', array( 'style' => 'text-align: center;', ), array( $create_button, $add_button, )); } }