Changeset View
Changeset View
Standalone View
Standalone View
src/applications/herald/controller/HeraldTranscriptController.php
<?php | <?php | ||||
final class HeraldTranscriptController extends HeraldController { | final class HeraldTranscriptController extends HeraldController { | ||||
const FILTER_AFFECTED = 'affected'; | |||||
const FILTER_OWNED = 'owned'; | |||||
const FILTER_ALL = 'all'; | |||||
private $id; | |||||
private $filter; | |||||
private $handles; | private $handles; | ||||
private $adapter; | private $adapter; | ||||
public function willProcessRequest(array $data) { | |||||
$this->id = $data['id']; | |||||
$map = $this->getFilterMap(); | |||||
$this->filter = idx($data, 'filter'); | |||||
if (empty($map[$this->filter])) { | |||||
$this->filter = self::FILTER_ALL; | |||||
} | |||||
} | |||||
private function getAdapter() { | private function getAdapter() { | ||||
return $this->adapter; | return $this->adapter; | ||||
} | } | ||||
public function processRequest() { | public function handleRequest(AphrontRequest $request) { | ||||
$request = $this->getRequest(); | $viewer = $this->getViewer(); | ||||
$viewer = $request->getUser(); | |||||
$xscript = id(new HeraldTranscriptQuery()) | $xscript = id(new HeraldTranscriptQuery()) | ||||
->setViewer($viewer) | ->setViewer($viewer) | ||||
->withIDs(array($this->id)) | ->withIDs(array($request->getURIData('id'))) | ||||
->executeOne(); | ->executeOne(); | ||||
if (!$xscript) { | if (!$xscript) { | ||||
return new Aphront404Response(); | return new Aphront404Response(); | ||||
} | } | ||||
require_celerity_resource('herald-test-css'); | require_celerity_resource('herald-test-css'); | ||||
$content = array(); | |||||
$nav = $this->buildSideNav(); | |||||
$object_xscript = $xscript->getObjectTranscript(); | $object_xscript = $xscript->getObjectTranscript(); | ||||
if (!$object_xscript) { | if (!$object_xscript) { | ||||
$notice = id(new PHUIInfoView()) | $notice = id(new PHUIInfoView()) | ||||
->setSeverity(PHUIInfoView::SEVERITY_NOTICE) | ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) | ||||
->setTitle(pht('Old Transcript')) | ->setTitle(pht('Old Transcript')) | ||||
->appendChild(phutil_tag( | ->appendChild(phutil_tag( | ||||
'p', | 'p', | ||||
array(), | array(), | ||||
pht('Details of this transcript have been garbage collected.'))); | pht('Details of this transcript have been garbage collected.'))); | ||||
$nav->appendChild($notice); | $content[] = $notice; | ||||
} else { | } else { | ||||
$map = HeraldAdapter::getEnabledAdapterMap($viewer); | $map = HeraldAdapter::getEnabledAdapterMap($viewer); | ||||
$object_type = $object_xscript->getType(); | $object_type = $object_xscript->getType(); | ||||
if (empty($map[$object_type])) { | if (empty($map[$object_type])) { | ||||
// TODO: We should filter these out in the Query, but we have to load | // TODO: We should filter these out in the Query, but we have to load | ||||
// the objectTranscript right now, which is potentially enormous. We | // the objectTranscript right now, which is potentially enormous. We | ||||
// should denormalize the object type, or move the data into a separate | // should denormalize the object type, or move the data into a separate | ||||
// table, and then filter this earlier (and thus raise a better error). | // table, and then filter this earlier (and thus raise a better error). | ||||
// For now, just block access so we don't violate policies. | // For now, just block access so we don't violate policies. | ||||
throw new Exception( | throw new Exception( | ||||
pht('This transcript has an invalid or inaccessible adapter.')); | pht('This transcript has an invalid or inaccessible adapter.')); | ||||
} | } | ||||
$this->adapter = HeraldAdapter::getAdapterForContentType($object_type); | $this->adapter = HeraldAdapter::getAdapterForContentType($object_type); | ||||
$filter = $this->getFilterPHIDs(); | $phids = $this->getTranscriptPHIDs($xscript); | ||||
$this->filterTranscript($xscript, $filter); | |||||
$phids = array_merge($filter, $this->getTranscriptPHIDs($xscript)); | |||||
$phids = array_unique($phids); | $phids = array_unique($phids); | ||||
$phids = array_filter($phids); | $phids = array_filter($phids); | ||||
$handles = $this->loadViewerHandles($phids); | $handles = $this->loadViewerHandles($phids); | ||||
$this->handles = $handles; | $this->handles = $handles; | ||||
if ($xscript->getDryRun()) { | if ($xscript->getDryRun()) { | ||||
$notice = new PHUIInfoView(); | $notice = new PHUIInfoView(); | ||||
$notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE); | $notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE); | ||||
$notice->setTitle(pht('Dry Run')); | $notice->setTitle(pht('Dry Run')); | ||||
$notice->appendChild( | $notice->appendChild( | ||||
pht( | pht( | ||||
'This was a dry run to test Herald rules, '. | 'This was a dry run to test Herald rules, '. | ||||
'no actions were executed.')); | 'no actions were executed.')); | ||||
$nav->appendChild($notice); | $content[] = $notice; | ||||
} | } | ||||
$warning_panel = $this->buildWarningPanel($xscript); | $warning_panel = $this->buildWarningPanel($xscript); | ||||
$nav->appendChild($warning_panel); | $content[] = $warning_panel; | ||||
$apply_xscript_panel = $this->buildApplyTranscriptPanel( | $content[] = array( | ||||
$xscript); | $this->buildActionTranscriptPanel($xscript), | ||||
$nav->appendChild($apply_xscript_panel); | $this->buildObjectTranscriptPanel($xscript), | ||||
); | |||||
$action_xscript_panel = $this->buildActionTranscriptPanel( | |||||
$xscript); | |||||
$nav->appendChild($action_xscript_panel); | |||||
$object_xscript_panel = $this->buildObjectTranscriptPanel( | |||||
$xscript); | |||||
$nav->appendChild($object_xscript_panel); | |||||
} | } | ||||
$crumbs = id($this->buildApplicationCrumbs()) | $crumbs = id($this->buildApplicationCrumbs()) | ||||
->addTextCrumb( | ->addTextCrumb( | ||||
pht('Transcripts'), | pht('Transcripts'), | ||||
$this->getApplicationURI('/transcript/')) | $this->getApplicationURI('/transcript/')) | ||||
->addTextCrumb($xscript->getID()); | ->addTextCrumb($xscript->getID()); | ||||
$nav->setCrumbs($crumbs); | |||||
return $this->buildApplicationPage( | return $this->buildApplicationPage( | ||||
$nav, | array( | ||||
$crumbs, | |||||
$content, | |||||
), | |||||
array( | array( | ||||
'title' => pht('Transcript'), | 'title' => pht('Transcript'), | ||||
)); | )); | ||||
} | } | ||||
protected function renderConditionTestValue($condition, $handles) { | protected function renderConditionTestValue($condition, $handles) { | ||||
// TODO: This is all a hacky mess and should be driven through FieldValue | // TODO: This is all a hacky mess and should be driven through FieldValue | ||||
// eventually. | // eventually. | ||||
Show All 20 Lines | if (!is_scalar($value) && $value !== null) { | ||||
} | } | ||||
sort($value); | sort($value); | ||||
$value = implode(', ', $value); | $value = implode(', ', $value); | ||||
} | } | ||||
return phutil_tag('span', array('class' => 'condition-test-value'), $value); | return phutil_tag('span', array('class' => 'condition-test-value'), $value); | ||||
} | } | ||||
private function buildSideNav() { | |||||
$nav = new AphrontSideNavFilterView(); | |||||
$nav->setBaseURI(new PhutilURI('/herald/transcript/'.$this->id.'/')); | |||||
$items = array(); | |||||
$filters = $this->getFilterMap(); | |||||
foreach ($filters as $key => $name) { | |||||
$nav->addFilter($key, $name); | |||||
} | |||||
$nav->selectFilter($this->filter, null); | |||||
return $nav; | |||||
} | |||||
protected function getFilterMap() { | |||||
return array( | |||||
self::FILTER_ALL => pht('All Rules'), | |||||
self::FILTER_OWNED => pht('Rules I Own'), | |||||
self::FILTER_AFFECTED => pht('Rules that Affected Me'), | |||||
); | |||||
} | |||||
protected function getFilterPHIDs() { | |||||
return array($this->getRequest()->getUser()->getPHID()); | |||||
} | |||||
protected function getTranscriptPHIDs($xscript) { | protected function getTranscriptPHIDs($xscript) { | ||||
$phids = array(); | $phids = array(); | ||||
$object_xscript = $xscript->getObjectTranscript(); | $object_xscript = $xscript->getObjectTranscript(); | ||||
if (!$object_xscript) { | if (!$object_xscript) { | ||||
return array(); | return array(); | ||||
} | } | ||||
Show All 39 Lines | foreach ($condition_xscripts as $condition_xscript) { | ||||
} | } | ||||
break; | break; | ||||
} | } | ||||
} | } | ||||
return $phids; | return $phids; | ||||
} | } | ||||
protected function filterTranscript($xscript, $filter_phids) { | |||||
$filter_owned = ($this->filter == self::FILTER_OWNED); | |||||
$filter_affected = ($this->filter == self::FILTER_AFFECTED); | |||||
if (!$filter_owned && !$filter_affected) { | |||||
// No filtering to be done. | |||||
return; | |||||
} | |||||
if (!$xscript->getObjectTranscript()) { | |||||
return; | |||||
} | |||||
$user_phid = $this->getRequest()->getUser()->getPHID(); | |||||
$keep_apply_xscripts = array(); | |||||
$keep_rule_xscripts = array(); | |||||
$filter_phids = array_fill_keys($filter_phids, true); | |||||
$rule_xscripts = $xscript->getRuleTranscripts(); | |||||
foreach ($xscript->getApplyTranscripts() as $id => $apply_xscript) { | |||||
$rule_id = $apply_xscript->getRuleID(); | |||||
if ($filter_owned) { | |||||
if (empty($rule_xscripts[$rule_id])) { | |||||
// No associated rule so you can't own this effect. | |||||
continue; | |||||
} | |||||
if ($rule_xscripts[$rule_id]->getRuleOwner() != $user_phid) { | |||||
continue; | |||||
} | |||||
} else if ($filter_affected) { | |||||
$targets = (array)$apply_xscript->getTarget(); | |||||
if (!array_select_keys($filter_phids, $targets)) { | |||||
continue; | |||||
} | |||||
} | |||||
$keep_apply_xscripts[$id] = true; | |||||
if ($rule_id) { | |||||
$keep_rule_xscripts[$rule_id] = true; | |||||
} | |||||
} | |||||
foreach ($rule_xscripts as $rule_id => $rule_xscript) { | |||||
if ($filter_owned && $rule_xscript->getRuleOwner() == $user_phid) { | |||||
$keep_rule_xscripts[$rule_id] = true; | |||||
} | |||||
} | |||||
$xscript->setRuleTranscripts( | |||||
array_intersect_key( | |||||
$xscript->getRuleTranscripts(), | |||||
$keep_rule_xscripts)); | |||||
$xscript->setApplyTranscripts( | |||||
array_intersect_key( | |||||
$xscript->getApplyTranscripts(), | |||||
$keep_apply_xscripts)); | |||||
$xscript->setConditionTranscripts( | |||||
array_intersect_key( | |||||
$xscript->getConditionTranscripts(), | |||||
$keep_rule_xscripts)); | |||||
} | |||||
private function buildWarningPanel(HeraldTranscript $xscript) { | private function buildWarningPanel(HeraldTranscript $xscript) { | ||||
$request = $this->getRequest(); | $request = $this->getRequest(); | ||||
$panel = null; | $panel = null; | ||||
if ($xscript->getObjectTranscript()) { | if ($xscript->getObjectTranscript()) { | ||||
$handles = $this->handles; | $handles = $this->handles; | ||||
$object_xscript = $xscript->getObjectTranscript(); | $object_xscript = $xscript->getObjectTranscript(); | ||||
$handle = $handles[$object_xscript->getPHID()]; | $handle = $handles[$object_xscript->getPHID()]; | ||||
if ($handle->getType() == | if ($handle->getType() == | ||||
Show All 24 Lines | if ($xscript->getObjectTranscript()) { | ||||
->setTitle($title) | ->setTitle($title) | ||||
->appendChild($body); | ->appendChild($body); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
return $panel; | return $panel; | ||||
} | } | ||||
private function buildApplyTranscriptPanel(HeraldTranscript $xscript) { | private function buildActionTranscriptPanel(HeraldTranscript $xscript) { | ||||
$handles = $this->handles; | $action_xscript = mgroup($xscript->getApplyTranscripts(), 'getRuleID'); | ||||
$adapter = $this->getAdapter(); | $adapter = $this->getAdapter(); | ||||
$rule_type_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL; | $field_names = $adapter->getFieldNameMap(); | ||||
$action_names = $adapter->getActionNameMap($rule_type_global); | $condition_names = $adapter->getConditionNameMap(); | ||||
$handles = $this->handles; | |||||
$list = new PHUIObjectItemListView(); | $action_map = $xscript->getApplyTranscripts(); | ||||
$list->setStates(true); | $action_map = mgroup($action_map, 'getRuleID'); | ||||
$list->setNoDataString(pht('No actions were taken.')); | |||||
foreach ($xscript->getApplyTranscripts() as $apply_xscript) { | |||||
$target = $apply_xscript->getTarget(); | $rule_list = id(new PHUIObjectItemListView()) | ||||
switch ($apply_xscript->getAction()) { | ->setNoDataString(pht('No Herald rules applied to this object.')); | ||||
case HeraldAdapter::ACTION_FLAG: | |||||
$target = PhabricatorFlagColor::getColorName($target); | |||||
break; | |||||
case HeraldAdapter::ACTION_BLOCK: | |||||
// Target is a text string. | |||||
$target = $target; | |||||
break; | |||||
default: | |||||
// TODO: This should be driven by HeraldActions. | |||||
if (is_array($target) && $target) { | foreach ($xscript->getRuleTranscripts() as $rule_xscript) { | ||||
foreach ($target as $k => $phid) { | $rule_id = $rule_xscript->getRuleID(); | ||||
if (isset($handles[$phid])) { | |||||
$target[$k] = $handles[$phid]->getName(); | |||||
} | |||||
} | |||||
$target = implode(', ', $target); | |||||
} else if (is_string($target)) { | |||||
$target = $target; | |||||
} else { | |||||
$target = '<empty>'; | |||||
} | |||||
break; | |||||
} | |||||
$item = new PHUIObjectItemView(); | $rule_item = id(new PHUIObjectItemView()) | ||||
->setObjectName(pht('H%d', $rule_id)) | |||||
->setHeader($rule_xscript->getRuleName()); | |||||
if ($apply_xscript->getApplied()) { | if (!$rule_xscript->getResult()) { | ||||
$item->setState(PHUIObjectItemView::STATE_SUCCESS); | $rule_item->setDisabled(true); | ||||
} else { | |||||
$item->setState(PHUIObjectItemView::STATE_FAIL); | |||||
} | } | ||||
$rule = idx( | $rule_list->addItem($rule_item); | ||||
$action_names, | |||||
$apply_xscript->getAction(), | |||||
pht('Unknown Action "%s"', $apply_xscript->getAction())); | |||||
$item->setHeader(pht('%s: %s', $rule, $target)); | // Build the field/condition transcript. | ||||
$item->addAttribute($apply_xscript->getReason()); | |||||
// TODO: This is a bit of a mess while actions convert. | $cond_xscripts = $xscript->getConditionTranscriptsForRule($rule_id); | ||||
$item->addAttribute( | $cond_list = id(new PHUIStatusListView()); | ||||
pht('Outcome: %s', $apply_xscript->getAppliedReason())); | $cond_list->addItem( | ||||
id(new PHUIStatusItemView()) | |||||
->setTarget(phutil_tag('strong', array(), pht('Conditions')))); | |||||
$list->addItem($item); | foreach ($cond_xscripts as $cond_xscript) { | ||||
if ($cond_xscript->getResult()) { | |||||
$icon = 'fa-check'; | |||||
$color = 'green'; | |||||
$result = pht('Passed'); | |||||
} else { | |||||
$icon = 'fa-times'; | |||||
$color = 'red'; | |||||
$result = pht('Failed'); | |||||
} | } | ||||
$box = new PHUIObjectBoxView(); | if ($cond_xscript->getNote()) { | ||||
$box->setHeaderText(pht('Actions Taken')); | $note = phutil_tag( | ||||
$box->appendChild($list); | 'div', | ||||
array( | |||||
'class' => 'herald-condition-note', | |||||
), | |||||
$cond_xscript->getNote()); | |||||
} else { | |||||
$note = null; | |||||
} | |||||
return $box; | // 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($result) | |||||
->setNote(array($explanation, $note)); | |||||
$cond_list->addItem($cond_item); | |||||
} | |||||
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; | |||||
} | } | ||||
private function buildActionTranscriptPanel(HeraldTranscript $xscript) { | $cond_box->addMargin(PHUI::MARGIN_MEDIUM_BOTTOM); | ||||
$action_xscript = mgroup($xscript->getApplyTranscripts(), 'getRuleID'); | |||||
$adapter = $this->getAdapter(); | $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); | |||||
$field_names = $adapter->getFieldNameMap(); | $action_list = id(new PHUIStatusListView()); | ||||
$condition_names = $adapter->getConditionNameMap(); | $action_list->addItem( | ||||
id(new PHUIStatusItemView()) | |||||
->setTarget(phutil_tag('strong', array(), $name))); | |||||
$handles = $this->handles; | $action_box = id(new PHUIBoxView()) | ||||
->appendChild($action_list) | |||||
->addMargin(PHUI::MARGIN_LARGE_LEFT); | |||||
$rule_markup = array(); | $rule_item->appendChild($action_box); | ||||
foreach ($xscript->getRuleTranscripts() as $rule_id => $rule) { | |||||
$cond_markup = array(); | |||||
foreach ($xscript->getConditionTranscriptsForRule($rule_id) as $cond) { | |||||
if ($cond->getNote()) { | |||||
$note = phutil_tag_div('herald-condition-note', $cond->getNote()); | |||||
} else { | |||||
$note = null; | |||||
} | |||||
if ($cond->getResult()) { | $log = $action_xscript->getAppliedReason(); | ||||
$result = phutil_tag( | |||||
'span', | |||||
array('class' => 'herald-outcome condition-pass'), | |||||
"\xE2\x9C\x93"); | |||||
} else { | |||||
$result = phutil_tag( | |||||
'span', | |||||
array('class' => 'herald-outcome condition-fail'), | |||||
"\xE2\x9C\x98"); | |||||
} | |||||
$cond_markup[] = phutil_tag( | // Handle older transcripts which used a static string to record | ||||
'li', | // action results. | ||||
array(), | if (!is_array($log)) { | ||||
$action_list->addItem( | |||||
id(new PHUIStatusItemView()) | |||||
->setIcon('fa-clock-o', 'grey') | |||||
->setTarget(pht('Old Transcript')) | |||||
->setNote( | |||||
pht( | pht( | ||||
'%s Condition: %s %s %s%s', | 'This is an old transcript which uses an obsolete log '. | ||||
$result, | 'format. Detailed action information is not available.'))); | ||||
idx($field_names, $cond->getFieldName(), pht('Unknown')), | continue; | ||||
idx($condition_names, $cond->getCondition(), pht('Unknown')), | |||||
$this->renderConditionTestValue($cond, $handles), | |||||
$note)); | |||||
} | } | ||||
if ($rule->getResult()) { | foreach ($log as $entry) { | ||||
$result = phutil_tag( | $type = idx($entry, 'type'); | ||||
'span', | $data = idx($entry, 'data'); | ||||
array('class' => 'herald-outcome rule-pass'), | |||||
pht('PASS')); | if ($action) { | ||||
$class = 'herald-rule-pass'; | $icon = $action->renderActionEffectIcon($type, $data); | ||||
$color = $action->renderActionEffectColor($type, $data); | |||||
$name = $action->renderActionEffectName($type, $data); | |||||
$note = $action->renderActionEffectDescription($type, $data); | |||||
} else { | } else { | ||||
$result = phutil_tag( | $icon = 'fa-question-circle'; | ||||
'span', | $color = 'indigo'; | ||||
array('class' => 'herald-outcome rule-fail'), | $name = pht('Unknown Effect ("%s")', $type); | ||||
pht('FAIL')); | $note = null; | ||||
$class = 'herald-rule-fail'; | |||||
} | } | ||||
$cond_markup[] = phutil_tag( | $action_item = id(new PHUIStatusItemView()) | ||||
'li', | ->setIcon($icon, $color) | ||||
array(), | ->setTarget($name) | ||||
array($result, $rule->getReason())); | ->setNote($note); | ||||
$user_phid = $this->getRequest()->getUser()->getPHID(); | |||||
$name = $rule->getRuleName(); | |||||
$rule_markup[] = | $action_list->addItem($action_item); | ||||
phutil_tag( | |||||
'li', | |||||
array( | |||||
'class' => $class, | |||||
), | |||||
phutil_tag_div('rule-name', array( | |||||
phutil_tag('strong', array(), $name), | |||||
' ', | |||||
phutil_tag('ul', array(), $cond_markup), | |||||
))); | |||||
} | } | ||||
$box = null; | |||||
if ($rule_markup) { | |||||
$box = new PHUIObjectBoxView(); | |||||
$box->setHeaderText(pht('Rule Details')); | |||||
$box->appendChild(phutil_tag( | |||||
'ul', | |||||
array('class' => 'herald-explain-list'), | |||||
$rule_markup)); | |||||
} | } | ||||
} | |||||
$box = id(new PHUIObjectBoxView()) | |||||
->setHeaderText(pht('Rule Transcript')) | |||||
->appendChild($rule_list); | |||||
return $box; | return $box; | ||||
} | } | ||||
private function buildObjectTranscriptPanel(HeraldTranscript $xscript) { | private function buildObjectTranscriptPanel(HeraldTranscript $xscript) { | ||||
$adapter = $this->getAdapter(); | $adapter = $this->getAdapter(); | ||||
$field_names = $adapter->getFieldNameMap(); | $field_names = $adapter->getFieldNameMap(); | ||||
▲ Show 20 Lines • Show All 59 Lines • Show Last 20 Lines |