@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at upstream/main 804 lines 23 kB view raw
1<?php 2 3final class HeraldTranscriptController extends HeraldController { 4 5 private $handles; 6 private $adapter; 7 8 private function getAdapter() { 9 return $this->adapter; 10 } 11 12 public function buildApplicationMenu() { 13 // Use the menu we build in this controller, not the default menu for 14 // Herald. 15 return null; 16 } 17 18 public function handleRequest(AphrontRequest $request) { 19 $viewer = $this->getViewer(); 20 21 $xscript = id(new HeraldTranscriptQuery()) 22 ->setViewer($viewer) 23 ->withIDs(array($request->getURIData('id'))) 24 ->executeOne(); 25 if (!$xscript) { 26 return new Aphront404Response(); 27 } 28 29 $view_key = $this->getViewKey($request); 30 if (!$view_key) { 31 return new Aphront404Response(); 32 } 33 34 $navigation = $this->newSideNavView($xscript, $view_key); 35 36 $object = $xscript->getObject(); 37 38 require_celerity_resource('herald-test-css'); 39 $content = array(); 40 41 $object_xscript = $xscript->getObjectTranscript(); 42 if (!$object_xscript) { 43 $notice = id(new PHUIInfoView()) 44 ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) 45 ->setTitle(pht('Old Transcript')) 46 ->appendChild(phutil_tag( 47 'p', 48 array(), 49 pht('Details of this transcript have been garbage collected.'))); 50 $content[] = $notice; 51 } else { 52 $map = HeraldAdapter::getEnabledAdapterMap($viewer); 53 $object_type = $object_xscript->getType(); 54 if (empty($map[$object_type])) { 55 // TODO: We should filter these out in the Query, but we have to load 56 // the objectTranscript right now, which is potentially enormous. We 57 // should denormalize the object type, or move the data into a separate 58 // table, and then filter this earlier (and thus raise a better error). 59 // For now, just block access so we don't violate policies. 60 throw new Exception( 61 pht('This transcript has an invalid or inaccessible adapter.')); 62 } 63 64 $this->adapter = HeraldAdapter::getAdapterForContentType($object_type); 65 66 $phids = $this->getTranscriptPHIDs($xscript); 67 $phids = array_unique($phids); 68 $phids = array_filter($phids); 69 70 $handles = $this->loadViewerHandles($phids); 71 $this->handles = $handles; 72 73 $warning_panel = $this->buildWarningPanel($xscript); 74 $content[] = $warning_panel; 75 76 $content[] = $this->newContentView($xscript, $view_key); 77 } 78 79 $crumbs = id($this->buildApplicationCrumbs()) 80 ->addTextCrumb( 81 pht('Transcripts'), 82 $this->getApplicationURI('/transcript/')) 83 ->addTextCrumb(pht('Transcript %d', $xscript->getID())) 84 ->setBorder(true); 85 86 $title = pht('Herald Transcript %s', $xscript->getID()); 87 $header = $this->newHeaderView($xscript, $title); 88 89 $view = id(new PHUITwoColumnView()) 90 ->setHeader($header) 91 ->setFooter($content); 92 93 return $this->newPage() 94 ->setTitle($title) 95 ->setCrumbs($crumbs) 96 ->setNavigation($navigation) 97 ->appendChild($view); 98 } 99 100 protected function renderConditionTestValue($condition, $handles) { 101 // TODO: This is all a hacky mess and should be driven through FieldValue 102 // eventually. 103 104 switch ($condition->getFieldName()) { 105 case HeraldAnotherRuleField::FIELDCONST: 106 $value = array($condition->getTestValue()); 107 break; 108 default: 109 $value = $condition->getTestValue(); 110 break; 111 } 112 113 if (!is_scalar($value) && $value !== null) { 114 foreach ($value as $key => $phid) { 115 $handle = idx($handles, $phid); 116 if ($handle && $handle->isComplete()) { 117 $value[$key] = $handle->getName(); 118 } else { 119 // This happens for things like task priorities, statuses, and 120 // custom fields. 121 $value[$key] = $phid; 122 } 123 } 124 sort($value); 125 $value = implode(', ', $value); 126 } 127 128 return phutil_tag('span', array('class' => 'condition-test-value'), $value); 129 } 130 131 protected function getTranscriptPHIDs($xscript) { 132 $phids = array(); 133 134 $object_xscript = $xscript->getObjectTranscript(); 135 if (!$object_xscript) { 136 return array(); 137 } 138 139 $phids[] = $object_xscript->getPHID(); 140 141 foreach ($xscript->getApplyTranscripts() as $apply_xscript) { 142 // TODO: This is total hacks. Add another amazing layer of abstraction. 143 $target = (array)$apply_xscript->getTarget(); 144 foreach ($target as $phid) { 145 if ($phid) { 146 $phids[] = $phid; 147 } 148 } 149 } 150 151 foreach ($xscript->getRuleTranscripts() as $rule_xscript) { 152 $phids[] = $rule_xscript->getRuleOwner(); 153 } 154 155 $condition_xscripts = $xscript->getConditionTranscripts(); 156 if ($condition_xscripts) { 157 $condition_xscripts = call_user_func_array( 158 'array_merge', 159 $condition_xscripts); 160 } 161 foreach ($condition_xscripts as $condition_xscript) { 162 switch ($condition_xscript->getFieldName()) { 163 case HeraldAnotherRuleField::FIELDCONST: 164 $phids[] = $condition_xscript->getTestValue(); 165 break; 166 default: 167 $value = $condition_xscript->getTestValue(); 168 // TODO: Also total hacks. 169 if (is_array($value)) { 170 foreach ($value as $phid) { 171 if ($phid) { 172 // TODO: Probably need to make sure this 173 // "looks like" a PHID or decrease the level of hacks here; 174 // this used to be an is_numeric() check in Facebook land. 175 $phids[] = $phid; 176 } 177 } 178 } 179 break; 180 } 181 } 182 183 return $phids; 184 } 185 186 private function buildWarningPanel(HeraldTranscript $xscript) { 187 $request = $this->getRequest(); 188 $panel = null; 189 if ($xscript->getObjectTranscript()) { 190 $handles = $this->handles; 191 $object_xscript = $xscript->getObjectTranscript(); 192 $handle = $handles[$object_xscript->getPHID()]; 193 if ($handle->getType() == 194 PhabricatorRepositoryCommitPHIDType::TYPECONST) { 195 $commit = id(new DiffusionCommitQuery()) 196 ->setViewer($request->getUser()) 197 ->withPHIDs(array($handle->getPHID())) 198 ->executeOne(); 199 if ($commit) { 200 $repository = $commit->getRepository(); 201 if ($repository->isImporting()) { 202 $title = pht( 203 'The %s repository is still importing.', 204 $repository->getMonogram()); 205 $body = pht( 206 'Herald rules will not trigger until import completes.'); 207 } else if (!$repository->isTracked()) { 208 $title = pht( 209 'The %s repository is not tracked.', 210 $repository->getMonogram()); 211 $body = pht( 212 'Herald rules will not trigger until tracking is enabled.'); 213 } else { 214 return $panel; 215 } 216 $panel = id(new PHUIInfoView()) 217 ->setSeverity(PHUIInfoView::SEVERITY_WARNING) 218 ->setTitle($title) 219 ->appendChild($body); 220 } 221 } 222 } 223 return $panel; 224 } 225 226 private function buildActionTranscriptPanel(HeraldTranscript $xscript) { 227 $viewer = $this->getViewer(); 228 $action_xscript = mgroup($xscript->getApplyTranscripts(), 'getRuleID'); 229 230 $adapter = $this->getAdapter(); 231 232 $field_names = $adapter->getFieldNameMap(); 233 $condition_names = $adapter->getConditionNameMap(); 234 235 $handles = $this->handles; 236 237 $action_map = $xscript->getApplyTranscripts(); 238 $action_map = mgroup($action_map, 'getRuleID'); 239 240 $rule_list = id(new PHUIObjectItemListView()) 241 ->setNoDataString(pht('No Herald rules applied to this object.')) 242 ->setFlush(true); 243 244 $rule_xscripts = $xscript->getRuleTranscripts(); 245 $rule_xscripts = msort($rule_xscripts, 'getRuleID'); 246 foreach ($rule_xscripts as $rule_xscript) { 247 $rule_id = $rule_xscript->getRuleID(); 248 249 $rule_monogram = pht('H%d', $rule_id); 250 $rule_uri = '/'.$rule_monogram; 251 252 $rule_item = id(new PHUIObjectItemView()) 253 ->setObjectName($rule_monogram) 254 ->setHeader($rule_xscript->getRuleName()) 255 ->setHref($rule_uri); 256 257 $rule_result = $rule_xscript->getRuleResult(); 258 259 if (!$rule_result->getShouldApplyActions()) { 260 $rule_item->setDisabled(true); 261 } 262 263 $rule_list->addItem($rule_item); 264 265 // Build the field/condition transcript. 266 267 $cond_xscripts = $xscript->getConditionTranscriptsForRule($rule_id); 268 269 $cond_list = new PHUIStatusListView(); 270 $cond_list->addItem( 271 id(new PHUIStatusItemView()) 272 ->setTarget(phutil_tag('strong', array(), pht('Conditions')))); 273 274 foreach ($cond_xscripts as $cond_xscript) { 275 $result = $cond_xscript->getResult(); 276 277 $icon = $result->getIconIcon(); 278 $color = $result->getIconColor(); 279 $name = $result->getName(); 280 281 $result_details = $result->newDetailsView($viewer); 282 if ($result_details !== null) { 283 $result_details = phutil_tag( 284 'div', 285 array( 286 'class' => 'herald-condition-note', 287 ), 288 $result_details); 289 } 290 291 // TODO: This is not really translatable and should be driven through 292 // HeraldField. 293 $explanation = pht( 294 '%s %s %s', 295 idx($field_names, $cond_xscript->getFieldName(), pht('Unknown')), 296 idx($condition_names, $cond_xscript->getCondition(), pht('Unknown')), 297 $this->renderConditionTestValue($cond_xscript, $handles)); 298 299 $cond_item = id(new PHUIStatusItemView()) 300 ->setIcon($icon, $color) 301 ->setTarget($name) 302 ->setNote(array($explanation, $result_details)); 303 304 $cond_list->addItem($cond_item); 305 } 306 307 $rule_result = $rule_xscript->getRuleResult(); 308 309 $last_icon = $rule_result->getIconIcon(); 310 $last_color = $rule_result->getIconColor(); 311 $last_result = $rule_result->getName(); 312 $last_note = $rule_result->getDescription(); 313 314 $last_details = $rule_result->newDetailsView($viewer); 315 if ($last_details !== null) { 316 $last_details = phutil_tag( 317 'div', 318 array( 319 'class' => 'herald-condition-note', 320 ), 321 $last_details); 322 } 323 324 $cond_last = id(new PHUIStatusItemView()) 325 ->setIcon($last_icon, $last_color) 326 ->setTarget(phutil_tag('strong', array(), $last_result)) 327 ->setNote(array($last_note, $last_details)); 328 $cond_list->addItem($cond_last); 329 330 $cond_box = id(new PHUIBoxView()) 331 ->appendChild($cond_list) 332 ->addMargin(PHUI::MARGIN_LARGE_LEFT); 333 334 $rule_item->appendChild($cond_box); 335 336 // Not all rules will have any action transcripts, but we show them 337 // in general because they may have relevant information even when 338 // rules did not take actions. In particular, state-based actions may 339 // forbid rules from matching. 340 341 $cond_box->addMargin(PHUI::MARGIN_MEDIUM_BOTTOM); 342 343 $action_xscripts = idx($action_map, $rule_id, array()); 344 foreach ($action_xscripts as $action_xscript) { 345 $action_key = $action_xscript->getAction(); 346 $action = $adapter->getActionImplementation($action_key); 347 348 if ($action) { 349 $name = $action->getHeraldActionName(); 350 $action->setViewer($this->getViewer()); 351 } else { 352 $name = pht('Unknown Action ("%s")', $action_key); 353 } 354 355 $name = pht('Action: %s', $name); 356 357 $action_list = new PHUIStatusListView(); 358 $action_list->addItem( 359 id(new PHUIStatusItemView()) 360 ->setTarget(phutil_tag('strong', array(), $name))); 361 362 $action_box = id(new PHUIBoxView()) 363 ->appendChild($action_list) 364 ->addMargin(PHUI::MARGIN_LARGE_LEFT); 365 366 $rule_item->appendChild($action_box); 367 368 $log = $action_xscript->getAppliedReason(); 369 370 // Handle older transcripts which used a static string to record 371 // action results. 372 373 if ($xscript->getDryRun()) { 374 $action_list->addItem( 375 id(new PHUIStatusItemView()) 376 ->setIcon('fa-ban', 'grey') 377 ->setTarget(pht('Dry Run')) 378 ->setNote( 379 pht( 380 'This was a dry run, so no actions were taken.'))); 381 continue; 382 } else if (!is_array($log)) { 383 $action_list->addItem( 384 id(new PHUIStatusItemView()) 385 ->setIcon('fa-clock-o', 'grey') 386 ->setTarget(pht('Old Transcript')) 387 ->setNote( 388 pht( 389 'This is an old transcript which uses an obsolete log '. 390 'format. Detailed action information is not available.'))); 391 continue; 392 } 393 394 foreach ($log as $entry) { 395 $type = idx($entry, 'type'); 396 $data = idx($entry, 'data'); 397 398 if ($action) { 399 $icon = $action->renderActionEffectIcon($type, $data); 400 $color = $action->renderActionEffectColor($type, $data); 401 $name = $action->renderActionEffectName($type, $data); 402 $note = $action->renderEffectDescription($type, $data); 403 } else { 404 $icon = 'fa-question-circle'; 405 $color = 'indigo'; 406 $name = pht('Unknown Effect ("%s")', $type); 407 $note = null; 408 } 409 410 $action_item = id(new PHUIStatusItemView()) 411 ->setIcon($icon, $color) 412 ->setTarget($name) 413 ->setNote($note); 414 415 $action_list->addItem($action_item); 416 } 417 } 418 } 419 420 $box = id(new PHUIObjectBoxView()) 421 ->setHeaderText(pht('Rule Transcript')) 422 ->appendChild($rule_list); 423 424 $content = array(); 425 426 if ($xscript->getDryRun()) { 427 $notice = new PHUIInfoView(); 428 $notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE); 429 $notice->setTitle(pht('Dry Run')); 430 $notice->appendChild( 431 pht( 432 'This was a dry run to test Herald rules, '. 433 'no actions were executed.')); 434 $content[] = $notice; 435 } 436 437 $content[] = $box; 438 439 return $content; 440 } 441 442 private function buildObjectTranscriptPanel(HeraldTranscript $xscript) { 443 $viewer = $this->getViewer(); 444 $adapter = $this->getAdapter(); 445 446 $field_names = $adapter->getFieldNameMap(); 447 448 $object_xscript = $xscript->getObjectTranscript(); 449 450 $rows = array(); 451 if ($object_xscript) { 452 $phid = $object_xscript->getPHID(); 453 $handles = $this->handles; 454 455 $rows[] = array( 456 pht('Object Name'), 457 $object_xscript->getName(), 458 ); 459 460 $rows[] = array( 461 pht('Object Type'), 462 $object_xscript->getType(), 463 ); 464 465 $rows[] = array( 466 pht('Object PHID'), 467 $phid, 468 ); 469 470 $rows[] = array( 471 pht('Object Link'), 472 $handles[$phid]->renderLink(), 473 ); 474 } 475 476 foreach ($xscript->getMetadataMap() as $key => $value) { 477 $rows[] = array( 478 $key, 479 $value, 480 ); 481 } 482 483 if ($object_xscript) { 484 foreach ($object_xscript->getFields() as $field_type => $value) { 485 if (isset($field_names[$field_type])) { 486 $field_name = pht('Field: %s', $field_names[$field_type]); 487 } else { 488 $field_name = pht('Unknown Field ("%s")', $field_type); 489 } 490 491 $field_value = $adapter->renderFieldTranscriptValue( 492 $viewer, 493 $field_type, 494 $value); 495 496 $rows[] = array( 497 $field_name, 498 $field_value, 499 ); 500 } 501 } 502 503 $property_list = new PHUIPropertyListView(); 504 $property_list->setStacked(true); 505 foreach ($rows as $row) { 506 $property_list->addProperty($row[0], $row[1]); 507 } 508 509 $box = new PHUIObjectBoxView(); 510 $box->setHeaderText(pht('Object Transcript')); 511 $box->appendChild($property_list); 512 513 return $box; 514 } 515 516 private function buildTransactionsTranscriptPanel(HeraldTranscript $xscript) { 517 $viewer = $this->getViewer(); 518 519 $xaction_phids = $this->getTranscriptTransactionPHIDs($xscript); 520 521 if ($xaction_phids) { 522 $object = $xscript->getObject(); 523 $query = PhabricatorApplicationTransactionQuery::newQueryForObject( 524 $object); 525 $xactions = $query 526 ->setViewer($viewer) 527 ->withPHIDs($xaction_phids) 528 ->execute(); 529 $xactions = mpull($xactions, null, 'getPHID'); 530 } else { 531 $xactions = array(); 532 } 533 534 $rows = array(); 535 foreach ($xaction_phids as $xaction_phid) { 536 $xaction = idx($xactions, $xaction_phid); 537 538 $xaction_identifier = $xaction_phid; 539 $xaction_date = null; 540 $xaction_display = null; 541 if ($xaction) { 542 $xaction_identifier = $xaction->getID(); 543 $xaction_date = phabricator_datetime( 544 $xaction->getDateCreated(), 545 $viewer); 546 547 // Since we don't usually render transactions outside of the context 548 // of objects, some of them might depend on missing object data. Out of 549 // an abundance of caution, catch any rendering issues. 550 try { 551 $xaction_display = $xaction->getTitle(); 552 } catch (Exception $ex) { 553 $xaction_display = $ex->getMessage(); 554 } 555 } 556 557 $rows[] = array( 558 $xaction_identifier, 559 $xaction_display, 560 $xaction_date, 561 ); 562 } 563 564 $table_view = id(new AphrontTableView($rows)) 565 ->setHeaders( 566 array( 567 pht('ID'), 568 pht('Transaction'), 569 pht('Date'), 570 )) 571 ->setColumnClasses( 572 array( 573 null, 574 'wide', 575 null, 576 )); 577 578 $box_view = id(new PHUIObjectBoxView()) 579 ->setHeaderText(pht('Transactions')) 580 ->setTable($table_view); 581 582 return $box_view; 583 } 584 585 586 private function buildProfilerTranscriptPanel(HeraldTranscript $xscript) { 587 $viewer = $this->getViewer(); 588 589 $object_xscript = $xscript->getObjectTranscript(); 590 591 $profile = $object_xscript->getProfile(); 592 593 // If this is an older transcript without profiler information, don't 594 // show anything. 595 if ($profile === null) { 596 return null; 597 } 598 599 $profile = isort($profile, 'elapsed'); 600 $profile = array_reverse($profile); 601 602 $phids = array(); 603 foreach ($profile as $frame) { 604 if ($frame['type'] === 'rule') { 605 $phids[] = $frame['key']; 606 } 607 } 608 $handles = $viewer->loadHandles($phids); 609 610 $field_map = HeraldField::getAllFields(); 611 612 $rows = array(); 613 foreach ($profile as $frame) { 614 $cost = $frame['elapsed']; 615 $cost = 1000000 * $cost; 616 $cost = pht('%s%ss', new PhutilNumber($cost), mb_chr(956, 'UTF-8')); 617 618 $type = $frame['type']; 619 switch ($type) { 620 case 'rule': 621 $type_display = pht('Rule'); 622 break; 623 case 'field': 624 $type_display = pht('Field'); 625 break; 626 default: 627 $type_display = $type; 628 break; 629 } 630 631 $key = $frame['key']; 632 switch ($type) { 633 case 'field': 634 $field_object = idx($field_map, $key); 635 if ($field_object) { 636 $key_display = $field_object->getHeraldFieldName(); 637 } else { 638 $key_display = $key; 639 } 640 break; 641 case 'rule': 642 $key_display = $handles[$key]->renderLink(); 643 break; 644 default: 645 $key_display = $key; 646 break; 647 } 648 649 $rows[] = array( 650 $type_display, 651 $key_display, 652 $cost, 653 pht('%s', new PhutilNumber($frame['count'])), 654 ); 655 } 656 657 $table_view = id(new AphrontTableView($rows)) 658 ->setHeaders( 659 array( 660 pht('Type'), 661 pht('What'), 662 pht('Cost'), 663 pht('Count'), 664 )) 665 ->setColumnClasses( 666 array( 667 null, 668 'wide', 669 'right', 670 'right', 671 )); 672 673 $box_view = id(new PHUIObjectBoxView()) 674 ->setHeaderText(pht('Profile')) 675 ->setTable($table_view); 676 677 return $box_view; 678 } 679 680 private function getViewKey(AphrontRequest $request) { 681 $view_key = $request->getURIData('view'); 682 683 if ($view_key === null) { 684 return 'rules'; 685 } 686 687 switch ($view_key) { 688 case 'fields': 689 case 'xactions': 690 case 'profile': 691 return $view_key; 692 default: 693 return null; 694 } 695 } 696 697 private function newSideNavView( 698 HeraldTranscript $xscript, 699 $view_key) { 700 701 $base_uri = urisprintf( 702 'transcript/%d/', 703 $xscript->getID()); 704 705 $base_uri = $this->getApplicationURI($base_uri); 706 $base_uri = new PhutilURI($base_uri); 707 708 $nav = id(new AphrontSideNavFilterView()) 709 ->setBaseURI($base_uri); 710 711 $nav->newLink('rules') 712 ->setHref($base_uri) 713 ->setName(pht('Rules')) 714 ->setIcon('fa-list-ul'); 715 716 $nav->newLink('fields') 717 ->setName(pht('Field Values')) 718 ->setIcon('fa-file-text-o'); 719 720 $has_xactions = $xscript->getObjectTranscript() 721 && $this->getTranscriptTransactionPHIDs($xscript); 722 723 $nav->newLink('xactions') 724 ->setName(pht('Transactions')) 725 ->setIcon('fa-forward') 726 ->setDisabled(!$has_xactions); 727 728 $nav->newLink('profile') 729 ->setName(pht('Profiler')) 730 ->setIcon('fa-tachometer'); 731 732 $nav->selectFilter($view_key); 733 734 return $nav; 735 } 736 737 private function newContentView( 738 HeraldTranscript $xscript, 739 $view_key) { 740 741 switch ($view_key) { 742 case 'rules': 743 $content = $this->buildActionTranscriptPanel($xscript); 744 break; 745 case 'fields': 746 $content = $this->buildObjectTranscriptPanel($xscript); 747 break; 748 case 'xactions': 749 $content = $this->buildTransactionsTranscriptPanel($xscript); 750 break; 751 case 'profile': 752 $content = $this->buildProfilerTranscriptPanel($xscript); 753 break; 754 default: 755 throw new Exception(pht('Unknown view key "%s".', $view_key)); 756 } 757 758 return $content; 759 } 760 761 private function getTranscriptTransactionPHIDs(HeraldTranscript $xscript) { 762 763 $object_xscript = $xscript->getObjectTranscript(); 764 $xaction_phids = $object_xscript->getAppliedTransactionPHIDs(); 765 766 // If the value is "null", this is an older transcript or this adapter 767 // does not use transactions. 768 // 769 // (If the value is "array()", this is a modern transcript which uses 770 // transactions, there just weren't any applied.) 771 if ($xaction_phids === null) { 772 return array(); 773 } 774 775 $object = $xscript->getObject(); 776 777 // If this object doesn't implement the right interface, we won't be 778 // able to load the transactions. 779 if (!($object instanceof PhabricatorApplicationTransactionInterface)) { 780 return array(); 781 } 782 783 return $xaction_phids; 784 } 785 786 private function newHeaderView(HeraldTranscript $xscript, $title) { 787 $header = id(new PHUIHeaderView()) 788 ->setHeader($title) 789 ->setHeaderIcon('fa-list-ul'); 790 791 if ($xscript->getDryRun()) { 792 $dry_run_tag = id(new PHUITagView()) 793 ->setType(PHUITagView::TYPE_SHADE) 794 ->setColor(PHUITagView::COLOR_VIOLET) 795 ->setName(pht('Dry Run')) 796 ->setIcon('fa-exclamation-triangle'); 797 798 $header->addTag($dry_run_tag); 799 } 800 801 return $header; 802 } 803 804}