adapter; } public function buildApplicationMenu() { // Use the menu we build in this controller, not the default menu for // Herald. return null; } public function handleRequest(AphrontRequest $request) { $viewer = $this->getViewer(); $xscript = id(new HeraldTranscriptQuery()) ->setViewer($viewer) ->withIDs(array($request->getURIData('id'))) ->executeOne(); if (!$xscript) { return new Aphront404Response(); } $view_key = $this->getViewKey($request); if (!$view_key) { return new Aphront404Response(); } $navigation = $this->newSideNavView($xscript, $view_key); $object = $xscript->getObject(); require_celerity_resource('herald-test-css'); $content = array(); $object_xscript = $xscript->getObjectTranscript(); if (!$object_xscript) { $notice = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setTitle(pht('Old Transcript')) ->appendChild(phutil_tag( 'p', array(), pht('Details of this transcript have been garbage collected.'))); $content[] = $notice; } else { $map = HeraldAdapter::getEnabledAdapterMap($viewer); $object_type = $object_xscript->getType(); if (empty($map[$object_type])) { // TODO: We should filter these out in the Query, but we have to load // the objectTranscript right now, which is potentially enormous. We // should denormalize the object type, or move the data into a separate // table, and then filter this earlier (and thus raise a better error). // For now, just block access so we don't violate policies. throw new Exception( pht('This transcript has an invalid or inaccessible adapter.')); } $this->adapter = HeraldAdapter::getAdapterForContentType($object_type); $phids = $this->getTranscriptPHIDs($xscript); $phids = array_unique($phids); $phids = array_filter($phids); $handles = $this->loadViewerHandles($phids); $this->handles = $handles; $warning_panel = $this->buildWarningPanel($xscript); $content[] = $warning_panel; $content[] = $this->newContentView($xscript, $view_key); } $crumbs = id($this->buildApplicationCrumbs()) ->addTextCrumb( pht('Transcripts'), $this->getApplicationURI('/transcript/')) ->addTextCrumb(pht('Transcript %d', $xscript->getID())) ->setBorder(true); $title = pht('Herald Transcript %s', $xscript->getID()); $header = $this->newHeaderView($xscript, $title); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter($content); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setNavigation($navigation) ->appendChild($view); } protected function renderConditionTestValue($condition, $handles) { // TODO: This is all a hacky mess and should be driven through FieldValue // eventually. switch ($condition->getFieldName()) { case HeraldAnotherRuleField::FIELDCONST: $value = array($condition->getTestValue()); break; default: $value = $condition->getTestValue(); break; } if (!is_scalar($value) && $value !== null) { foreach ($value as $key => $phid) { $handle = idx($handles, $phid); if ($handle && $handle->isComplete()) { $value[$key] = $handle->getName(); } else { // This happens for things like task priorities, statuses, and // custom fields. $value[$key] = $phid; } } sort($value); $value = implode(', ', $value); } return phutil_tag('span', array('class' => 'condition-test-value'), $value); } protected function getTranscriptPHIDs($xscript) { $phids = array(); $object_xscript = $xscript->getObjectTranscript(); if (!$object_xscript) { return array(); } $phids[] = $object_xscript->getPHID(); foreach ($xscript->getApplyTranscripts() as $apply_xscript) { // TODO: This is total hacks. Add another amazing layer of abstraction. $target = (array)$apply_xscript->getTarget(); foreach ($target as $phid) { if ($phid) { $phids[] = $phid; } } } foreach ($xscript->getRuleTranscripts() as $rule_xscript) { $phids[] = $rule_xscript->getRuleOwner(); } $condition_xscripts = $xscript->getConditionTranscripts(); if ($condition_xscripts) { $condition_xscripts = call_user_func_array( 'array_merge', $condition_xscripts); } foreach ($condition_xscripts as $condition_xscript) { switch ($condition_xscript->getFieldName()) { case HeraldAnotherRuleField::FIELDCONST: $phids[] = $condition_xscript->getTestValue(); break; default: $value = $condition_xscript->getTestValue(); // TODO: Also total hacks. if (is_array($value)) { foreach ($value as $phid) { if ($phid) { // TODO: Probably need to make sure this // "looks like" a PHID or decrease the level of hacks here; // this used to be an is_numeric() check in Facebook land. $phids[] = $phid; } } } break; } } return $phids; } private function buildWarningPanel(HeraldTranscript $xscript) { $request = $this->getRequest(); $panel = null; if ($xscript->getObjectTranscript()) { $handles = $this->handles; $object_xscript = $xscript->getObjectTranscript(); $handle = $handles[$object_xscript->getPHID()]; if ($handle->getType() == PhabricatorRepositoryCommitPHIDType::TYPECONST) { $commit = id(new DiffusionCommitQuery()) ->setViewer($request->getUser()) ->withPHIDs(array($handle->getPHID())) ->executeOne(); if ($commit) { $repository = $commit->getRepository(); if ($repository->isImporting()) { $title = pht( 'The %s repository is still importing.', $repository->getMonogram()); $body = pht( 'Herald rules will not trigger until import completes.'); } else if (!$repository->isTracked()) { $title = pht( 'The %s repository is not tracked.', $repository->getMonogram()); $body = pht( 'Herald rules will not trigger until tracking is enabled.'); } else { return $panel; } $panel = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setTitle($title) ->appendChild($body); } } } return $panel; } private function buildActionTranscriptPanel(HeraldTranscript $xscript) { $action_xscript = mgroup($xscript->getApplyTranscripts(), 'getRuleID'); $adapter = $this->getAdapter(); $field_names = $adapter->getFieldNameMap(); $condition_names = $adapter->getConditionNameMap(); $handles = $this->handles; $action_map = $xscript->getApplyTranscripts(); $action_map = mgroup($action_map, 'getRuleID'); $rule_list = id(new PHUIObjectItemListView()) ->setNoDataString(pht('No Herald rules applied to this object.')) ->setFlush(true); $rule_xscripts = $xscript->getRuleTranscripts(); $rule_xscripts = msort($rule_xscripts, 'getRuleID'); foreach ($rule_xscripts as $rule_xscript) { $rule_id = $rule_xscript->getRuleID(); $rule_monogram = pht('H%d', $rule_id); $rule_uri = '/'.$rule_monogram; $rule_item = id(new PHUIObjectItemView()) ->setObjectName($rule_monogram) ->setHeader($rule_xscript->getRuleName()) ->setHref($rule_uri); if (!$rule_xscript->getResult()) { $rule_item->setDisabled(true); } $rule_list->addItem($rule_item); // Build the field/condition transcript. $cond_xscripts = $xscript->getConditionTranscriptsForRule($rule_id); $cond_list = id(new PHUIStatusListView()); $cond_list->addItem( id(new PHUIStatusItemView()) ->setTarget(phutil_tag('strong', array(), pht('Conditions')))); foreach ($cond_xscripts as $cond_xscript) { $result = $cond_xscript->getResult(); $icon = $result->getIconIcon(); $color = $result->getIconColor(); $name = $result->getName(); $result_details = $result->newDetailsView(); if ($result_details !== null) { $result_details = phutil_tag( 'div', array( 'class' => 'herald-condition-note', ), $result_details); } // TODO: This is not really translatable and should be driven through // HeraldField. $explanation = pht( '%s %s %s', idx($field_names, $cond_xscript->getFieldName(), pht('Unknown')), idx($condition_names, $cond_xscript->getCondition(), pht('Unknown')), $this->renderConditionTestValue($cond_xscript, $handles)); $cond_item = id(new PHUIStatusItemView()) ->setIcon($icon, $color) ->setTarget($name) ->setNote(array($explanation, $result_details)); $cond_list->addItem($cond_item); } if ($rule_xscript->isForbidden()) { $last_icon = 'fa-ban'; $last_color = 'indigo'; $last_result = pht('Forbidden'); $last_note = pht('Object state prevented rule evaluation.'); } else if ($rule_xscript->getResult()) { $last_icon = 'fa-check-circle'; $last_color = 'green'; $last_result = pht('Passed'); $last_note = pht('Rule passed.'); } else { $last_icon = 'fa-times-circle'; $last_color = 'red'; $last_result = pht('Failed'); $last_note = pht('Rule failed.'); } $cond_last = id(new PHUIStatusItemView()) ->setIcon($last_icon, $last_color) ->setTarget(phutil_tag('strong', array(), $last_result)) ->setNote($last_note); $cond_list->addItem($cond_last); $cond_box = id(new PHUIBoxView()) ->appendChild($cond_list) ->addMargin(PHUI::MARGIN_LARGE_LEFT); $rule_item->appendChild($cond_box); if (!$rule_xscript->getResult()) { // If the rule didn't pass, don't generate an action transcript since // actions didn't apply. continue; } $cond_box->addMargin(PHUI::MARGIN_MEDIUM_BOTTOM); $action_xscripts = idx($action_map, $rule_id, array()); foreach ($action_xscripts as $action_xscript) { $action_key = $action_xscript->getAction(); $action = $adapter->getActionImplementation($action_key); if ($action) { $name = $action->getHeraldActionName(); $action->setViewer($this->getViewer()); } else { $name = pht('Unknown Action ("%s")', $action_key); } $name = pht('Action: %s', $name); $action_list = id(new PHUIStatusListView()); $action_list->addItem( id(new PHUIStatusItemView()) ->setTarget(phutil_tag('strong', array(), $name))); $action_box = id(new PHUIBoxView()) ->appendChild($action_list) ->addMargin(PHUI::MARGIN_LARGE_LEFT); $rule_item->appendChild($action_box); $log = $action_xscript->getAppliedReason(); // Handle older transcripts which used a static string to record // action results. if ($xscript->getDryRun()) { $action_list->addItem( id(new PHUIStatusItemView()) ->setIcon('fa-ban', 'grey') ->setTarget(pht('Dry Run')) ->setNote( pht( 'This was a dry run, so no actions were taken.'))); continue; } else if (!is_array($log)) { $action_list->addItem( id(new PHUIStatusItemView()) ->setIcon('fa-clock-o', 'grey') ->setTarget(pht('Old Transcript')) ->setNote( pht( 'This is an old transcript which uses an obsolete log '. 'format. Detailed action information is not available.'))); continue; } foreach ($log as $entry) { $type = idx($entry, 'type'); $data = idx($entry, 'data'); if ($action) { $icon = $action->renderActionEffectIcon($type, $data); $color = $action->renderActionEffectColor($type, $data); $name = $action->renderActionEffectName($type, $data); $note = $action->renderEffectDescription($type, $data); } else { $icon = 'fa-question-circle'; $color = 'indigo'; $name = pht('Unknown Effect ("%s")', $type); $note = null; } $action_item = id(new PHUIStatusItemView()) ->setIcon($icon, $color) ->setTarget($name) ->setNote($note); $action_list->addItem($action_item); } } } $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Rule Transcript')) ->appendChild($rule_list); $content = array(); if ($xscript->getDryRun()) { $notice = new PHUIInfoView(); $notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $notice->setTitle(pht('Dry Run')); $notice->appendChild( pht( 'This was a dry run to test Herald rules, '. 'no actions were executed.')); $content[] = $notice; } $content[] = $box; return $content; } private function buildObjectTranscriptPanel(HeraldTranscript $xscript) { $viewer = $this->getViewer(); $adapter = $this->getAdapter(); $field_names = $adapter->getFieldNameMap(); $object_xscript = $xscript->getObjectTranscript(); $rows = array(); if ($object_xscript) { $phid = $object_xscript->getPHID(); $handles = $this->handles; $rows[] = array( pht('Object Name'), $object_xscript->getName(), ); $rows[] = array( pht('Object Type'), $object_xscript->getType(), ); $rows[] = array( pht('Object PHID'), $phid, ); $rows[] = array( pht('Object Link'), $handles[$phid]->renderLink(), ); } foreach ($xscript->getMetadataMap() as $key => $value) { $rows[] = array( $key, $value, ); } if ($object_xscript) { foreach ($object_xscript->getFields() as $field_type => $value) { if (isset($field_names[$field_type])) { $field_name = pht('Field: %s', $field_names[$field_type]); } else { $field_name = pht('Unknown Field ("%s")', $field_type); } $field_value = $adapter->renderFieldTranscriptValue( $viewer, $field_type, $value); $rows[] = array( $field_name, $field_value, ); } } $property_list = new PHUIPropertyListView(); $property_list->setStacked(true); foreach ($rows as $row) { $property_list->addProperty($row[0], $row[1]); } $box = new PHUIObjectBoxView(); $box->setHeaderText(pht('Object Transcript')); $box->appendChild($property_list); return $box; } private function buildTransactionsTranscriptPanel(HeraldTranscript $xscript) { $viewer = $this->getViewer(); $xaction_phids = $this->getTranscriptTransactionPHIDs($xscript); if ($xaction_phids) { $object = $xscript->getObject(); $query = PhabricatorApplicationTransactionQuery::newQueryForObject( $object); $xactions = $query ->setViewer($viewer) ->withPHIDs($xaction_phids) ->execute(); $xactions = mpull($xactions, null, 'getPHID'); } else { $xactions = array(); } $rows = array(); foreach ($xaction_phids as $xaction_phid) { $xaction = idx($xactions, $xaction_phid); $xaction_identifier = $xaction_phid; $xaction_date = null; $xaction_display = null; if ($xaction) { $xaction_identifier = $xaction->getID(); $xaction_date = phabricator_datetime( $xaction->getDateCreated(), $viewer); // Since we don't usually render transactions outside of the context // of objects, some of them might depend on missing object data. Out of // an abundance of caution, catch any rendering issues. try { $xaction_display = $xaction->getTitle(); } catch (Exception $ex) { $xaction_display = $ex->getMessage(); } } $rows[] = array( $xaction_identifier, $xaction_display, $xaction_date, ); } $table_view = id(new AphrontTableView($rows)) ->setHeaders( array( pht('ID'), pht('Transaction'), pht('Date'), )) ->setColumnClasses( array( null, 'wide', null, )); $box_view = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Transactions')) ->setTable($table_view); return $box_view; } private function buildProfilerTranscriptPanel(HeraldTranscript $xscript) { $viewer = $this->getViewer(); $object_xscript = $xscript->getObjectTranscript(); $profile = $object_xscript->getProfile(); // If this is an older transcript without profiler information, don't // show anything. if ($profile === null) { return null; } $profile = isort($profile, 'elapsed'); $profile = array_reverse($profile); $phids = array(); foreach ($profile as $frame) { if ($frame['type'] === 'rule') { $phids[] = $frame['key']; } } $handles = $viewer->loadHandles($phids); $field_map = HeraldField::getAllFields(); $rows = array(); foreach ($profile as $frame) { $cost = $frame['elapsed']; $cost = 1000000 * $cost; $cost = pht('%sus', new PhutilNumber($cost)); $type = $frame['type']; switch ($type) { case 'rule': $type_display = pht('Rule'); break; case 'field': $type_display = pht('Field'); break; default: $type_display = $type; break; } $key = $frame['key']; switch ($type) { case 'field': $field_object = idx($field_map, $key); if ($field_object) { $key_display = $field_object->getHeraldFieldName(); } else { $key_display = $key; } break; case 'rule': $key_display = $handles[$key]->renderLink(); break; default: $key_display = $key; break; } $rows[] = array( $type_display, $key_display, $cost, pht('%s', new PhutilNumber($frame['count'])), ); } $table_view = id(new AphrontTableView($rows)) ->setHeaders( array( pht('Type'), pht('What'), pht('Cost'), pht('Count'), )) ->setColumnClasses( array( null, 'wide', 'right', 'right', )); $box_view = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Profile')) ->setTable($table_view); return $box_view; } private function getViewKey(AphrontRequest $request) { $view_key = $request->getURIData('view'); if ($view_key === null) { return 'rules'; } switch ($view_key) { case 'fields': case 'xactions': case 'profile': return $view_key; default: return null; } } private function newSideNavView( HeraldTranscript $xscript, $view_key) { $base_uri = urisprintf( 'transcript/%d/', $xscript->getID()); $base_uri = $this->getApplicationURI($base_uri); $base_uri = new PhutilURI($base_uri); $nav = id(new AphrontSideNavFilterView()) ->setBaseURI($base_uri); $nav->newLink('rules') ->setHref($base_uri) ->setName(pht('Rules')) ->setIcon('fa-list-ul'); $nav->newLink('fields') ->setName(pht('Field Values')) ->setIcon('fa-file-text-o'); $xaction_phids = $this->getTranscriptTransactionPHIDs($xscript); $has_xactions = (bool)$xaction_phids; $nav->newLink('xactions') ->setName(pht('Transactions')) ->setIcon('fa-forward') ->setDisabled(!$has_xactions); $nav->newLink('profile') ->setName(pht('Profiler')) ->setIcon('fa-tachometer'); $nav->selectFilter($view_key); return $nav; } private function newContentView( HeraldTranscript $xscript, $view_key) { switch ($view_key) { case 'rules': $content = $this->buildActionTranscriptPanel($xscript); break; case 'fields': $content = $this->buildObjectTranscriptPanel($xscript); break; case 'xactions': $content = $this->buildTransactionsTranscriptPanel($xscript); break; case 'profile': $content = $this->buildProfilerTranscriptPanel($xscript); break; default: throw new Exception(pht('Unknown view key "%s".', $view_key)); } return $content; } private function getTranscriptTransactionPHIDs(HeraldTranscript $xscript) { $object_xscript = $xscript->getObjectTranscript(); $xaction_phids = $object_xscript->getAppliedTransactionPHIDs(); // If the value is "null", this is an older transcript or this adapter // does not use transactions. // // (If the value is "array()", this is a modern transcript which uses // transactions, there just weren't any applied.) if ($xaction_phids === null) { return array(); } $object = $xscript->getObject(); // If this object doesn't implement the right interface, we won't be // able to load the transactions. if (!($object instanceof PhabricatorApplicationTransactionInterface)) { return array(); } return $xaction_phids; } private function newHeaderView(HeraldTranscript $xscript, $title) { $header = id(new PHUIHeaderView()) ->setHeader($title) ->setHeaderIcon('fa-list-ul'); if ($xscript->getDryRun()) { $dry_run_tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setColor(PHUITagView::COLOR_VIOLET) ->setName(pht('Dry Run')) ->setIcon('fa-exclamation-triangle'); $header->addTag($dry_run_tag); } return $header; } }