1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-23 14:00:56 +01:00

On Harbormaster build plans, show which Herald rules trigger builds

Summary:
Ref T13258. Provide an easy way to find rules which trigger a particular build plan from the build plan page.

The implementation here ends up a little messy: we can't just search for `actionType = 'build' AND targetPHID = '<build plan PHID>'` since the field is a blob of JSON.

Instead, make rules indexable and write a "build plan is affected by rule actions" edge when indexing rules, then search on that edge.

For now, only "Run Build Plan: ..." rules actually write this edge, since I think (?) that it doesn't really have meaningful values for other edge types today. Maybe "Call Webhooks", and you could get a link from a hook to rules that trigger it? Reasonable to do in the future.

Things end up a little bit rough overall, but I think this panel is pretty useful to add to the Build Plan page.

This index needs to be rebuilt with `bin/search index --type HeraldRule`. I'll call this out in the changelog but I'm not planning to explicitly migrate or add an activity, since this is only really important for larger installs and they probably (?) read the changelog. As rules are edited over time, this will converge to the right behavior even if you don't rebuild the index.

Test Plan: {F6260095}

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T13258

Differential Revision: https://secure.phabricator.com/D20259
This commit is contained in:
epriestley 2019-03-07 06:10:11 -08:00
parent 77221bee72
commit 950a7bbb19
11 changed files with 264 additions and 40 deletions

View file

@ -1531,6 +1531,7 @@ phutil_register_library_map(array(
'HeraldRemarkupFieldValue' => 'applications/herald/value/HeraldRemarkupFieldValue.php',
'HeraldRemarkupRule' => 'applications/herald/remarkup/HeraldRemarkupRule.php',
'HeraldRule' => 'applications/herald/storage/HeraldRule.php',
'HeraldRuleActionAffectsObjectEdgeType' => 'applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php',
'HeraldRuleAdapter' => 'applications/herald/adapter/HeraldRuleAdapter.php',
'HeraldRuleAdapterField' => 'applications/herald/field/rule/HeraldRuleAdapterField.php',
'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php',
@ -1540,7 +1541,9 @@ phutil_register_library_map(array(
'HeraldRuleEditor' => 'applications/herald/editor/HeraldRuleEditor.php',
'HeraldRuleField' => 'applications/herald/field/rule/HeraldRuleField.php',
'HeraldRuleFieldGroup' => 'applications/herald/field/rule/HeraldRuleFieldGroup.php',
'HeraldRuleIndexEngineExtension' => 'applications/herald/engineextension/HeraldRuleIndexEngineExtension.php',
'HeraldRuleListController' => 'applications/herald/controller/HeraldRuleListController.php',
'HeraldRuleListView' => 'applications/herald/view/HeraldRuleListView.php',
'HeraldRuleNameTransaction' => 'applications/herald/xaction/HeraldRuleNameTransaction.php',
'HeraldRulePHIDType' => 'applications/herald/phid/HeraldRulePHIDType.php',
'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php',
@ -7189,8 +7192,10 @@ phutil_register_library_map(array(
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorIndexableInterface',
'PhabricatorSubscribableInterface',
),
'HeraldRuleActionAffectsObjectEdgeType' => 'PhabricatorEdgeType',
'HeraldRuleAdapter' => 'HeraldAdapter',
'HeraldRuleAdapterField' => 'HeraldRuleField',
'HeraldRuleController' => 'HeraldController',
@ -7200,7 +7205,9 @@ phutil_register_library_map(array(
'HeraldRuleEditor' => 'PhabricatorApplicationTransactionEditor',
'HeraldRuleField' => 'HeraldField',
'HeraldRuleFieldGroup' => 'HeraldFieldGroup',
'HeraldRuleIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'HeraldRuleListController' => 'HeraldController',
'HeraldRuleListView' => 'AphrontView',
'HeraldRuleNameTransaction' => 'HeraldRuleTransactionType',
'HeraldRulePHIDType' => 'PhabricatorPHIDType',
'HeraldRuleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',

View file

@ -62,6 +62,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
$builds_view = $this->newBuildsView($plan);
$options_view = $this->newOptionsView($plan);
$rules_view = $this->newRulesView($plan);
$timeline = $this->buildTransactionTimeline(
$plan,
@ -76,6 +77,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
$error,
$step_list,
$options_view,
$rules_view,
$builds_view,
$timeline,
));
@ -486,6 +488,42 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
->appendChild($list);
}
private function newRulesView(HarbormasterBuildPlan $plan) {
$viewer = $this->getViewer();
$rules = id(new HeraldRuleQuery())
->setViewer($viewer)
->withDisabled(false)
->withAffectedObjectPHIDs(array($plan->getPHID()))
->needValidateAuthors(true)
->execute();
$list = id(new HeraldRuleListView())
->setViewer($viewer)
->setRules($rules)
->newObjectList();
$list->setNoDataString(pht('No active Herald rules trigger this build.'));
$more_href = new PhutilURI(
'/herald/',
array('affectedPHID' => $plan->getPHID()));
$more_link = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-list-ul')
->setText(pht('View All Rules'))
->setHref($more_href);
$header = id(new PHUIHeaderView())
->setHeader(pht('Run By Herald Rules'))
->addActionLink($more_link);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($list);
}
private function newOptionsView(HarbormasterBuildPlan $plan) {
$viewer = $this->getViewer();

View file

@ -91,4 +91,9 @@ final class HarbormasterRunBuildPlansHeraldAction
'Run build plans: %s.',
$this->renderHandleList($value));
}
public function getPHIDsAffectedByAction(HeraldActionRecord $record) {
return $record->getTarget();
}
}

View file

@ -401,4 +401,8 @@ abstract class HeraldAction extends Phobject {
return null;
}
public function getPHIDsAffectedByAction(HeraldActionRecord $record) {
return array();
}
}

View file

@ -0,0 +1,8 @@
<?php
final class HeraldRuleActionAffectsObjectEdgeType
extends PhabricatorEdgeType {
const EDGECONST = 69;
}

View file

@ -74,4 +74,8 @@ final class HeraldRuleEditor
return $body;
}
protected function supportsSearch() {
return true;
}
}

View file

@ -0,0 +1,92 @@
<?php
final class HeraldRuleIndexEngineExtension
extends PhabricatorIndexEngineExtension {
const EXTENSIONKEY = 'herald.actions';
public function getExtensionName() {
return pht('Herald Actions');
}
public function shouldIndexObject($object) {
if (!($object instanceof HeraldRule)) {
return false;
}
return true;
}
public function indexObject(
PhabricatorIndexEngine $engine,
$object) {
$edge_type = HeraldRuleActionAffectsObjectEdgeType::EDGECONST;
$old_edges = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
$edge_type);
$old_edges = array_fuse($old_edges);
$new_edges = $this->getPHIDsAffectedByActions($object);
$new_edges = array_fuse($new_edges);
$add_edges = array_diff_key($new_edges, $old_edges);
$rem_edges = array_diff_key($old_edges, $new_edges);
if (!$add_edges && !$rem_edges) {
return;
}
$editor = new PhabricatorEdgeEditor();
foreach ($add_edges as $phid) {
$editor->addEdge($object->getPHID(), $edge_type, $phid);
}
foreach ($rem_edges as $phid) {
$editor->removeEdge($object->getPHID(), $edge_type, $phid);
}
$editor->save();
}
public function getIndexVersion($object) {
$phids = $this->getPHIDsAffectedByActions($object);
sort($phids);
$phids = implode(':', $phids);
return PhabricatorHash::digestForIndex($phids);
}
private function getPHIDsAffectedByActions(HeraldRule $rule) {
$viewer = $this->getViewer();
$rule = id(new HeraldRuleQuery())
->setViewer($viewer)
->withIDs(array($rule->getID()))
->needConditionsAndActions(true)
->executeOne();
if (!$rule) {
return array();
}
$phids = array();
$actions = HeraldAction::getAllActions();
foreach ($rule->getActions() as $action_record) {
$action = idx($actions, $action_record->getAction());
if (!$action) {
continue;
}
foreach ($action->getPHIDsAffectedByAction($action_record) as $phid) {
$phids[] = $phid;
}
}
$phids = array_fuse($phids);
return array_keys($phids);
}
}

View file

@ -11,6 +11,7 @@ final class HeraldRuleQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $active;
private $datasourceQuery;
private $triggerObjectPHIDs;
private $affectedObjectPHIDs;
private $needConditionsAndActions;
private $needAppliedToPHIDs;
@ -61,6 +62,11 @@ final class HeraldRuleQuery extends PhabricatorCursorPagedPolicyAwareQuery {
return $this;
}
public function withAffectedObjectPHIDs(array $phids) {
$this->affectedObjectPHIDs = $phids;
return $this;
}
public function needConditionsAndActions($need) {
$this->needConditionsAndActions = $need;
return $this;
@ -261,9 +267,31 @@ final class HeraldRuleQuery extends PhabricatorCursorPagedPolicyAwareQuery {
$this->triggerObjectPHIDs);
}
if ($this->affectedObjectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'edge_affects.dst IN (%Ls)',
$this->affectedObjectPHIDs);
}
return $where;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->affectedObjectPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T edge_affects ON rule.phid = edge_affects.src
AND edge_affects.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
HeraldRuleActionAffectsObjectEdgeType::EDGECONST);
}
return $joins;
}
private function validateRuleAuthors(array $rules) {
// "Global" and "Object" rules always have valid authors.
foreach ($rules as $key => $rule) {

View file

@ -55,6 +55,10 @@ final class HeraldRuleSearchEngine extends PhabricatorApplicationSearchEngine {
pht('(Show All)'),
pht('Show Only Disabled Rules'),
pht('Show Only Enabled Rules')),
id(new PhabricatorPHIDsSearchField())
->setLabel(pht('Affected Objects'))
->setKey('affectedPHIDs')
->setAliases(array('affectedPHID')),
);
}
@ -81,6 +85,10 @@ final class HeraldRuleSearchEngine extends PhabricatorApplicationSearchEngine {
$query->withActive($map['active']);
}
if ($map['affectedPHIDs']) {
$query->withAffectedObjectPHIDs($map['affectedPHIDs']);
}
return $query;
}
@ -127,54 +135,18 @@ final class HeraldRuleSearchEngine extends PhabricatorApplicationSearchEngine {
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($rules, 'HeraldRule');
$viewer = $this->requireViewer();
$handles = $viewer->loadHandles(mpull($rules, 'getAuthorPHID'));
$content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer);
$list = id(new PHUIObjectItemListView())
->setUser($viewer);
foreach ($rules as $rule) {
$monogram = $rule->getMonogram();
$item = id(new PHUIObjectItemView())
->setObjectName($monogram)
->setHeader($rule->getName())
->setHref("/{$monogram}");
if ($rule->isPersonalRule()) {
$item->addIcon('fa-user', pht('Personal Rule'));
$item->addByline(
pht(
'Authored by %s',
$handles[$rule->getAuthorPHID()]->renderLink()));
} else if ($rule->isObjectRule()) {
$item->addIcon('fa-briefcase', pht('Object Rule'));
} else {
$item->addIcon('fa-globe', pht('Global Rule'));
}
if ($rule->getIsDisabled()) {
$item->setDisabled(true);
$item->addIcon('fa-lock grey', pht('Disabled'));
} else if (!$rule->hasValidAuthor()) {
$item->setDisabled(true);
$item->addIcon('fa-user grey', pht('Author Not Active'));
}
$content_type_name = idx($content_type_map, $rule->getContentType());
$item->addAttribute(pht('Affects: %s', $content_type_name));
$list->addItem($item);
}
$list = id(new HeraldRuleListView())
->setViewer($viewer)
->setRules($rules)
->newObjectList();
$result = new PhabricatorApplicationSearchResultView();
$result->setObjectList($list);
$result->setNoDataString(pht('No rules found.'));
return $result;
}
protected function getNewUserBody() {

View file

@ -6,6 +6,7 @@ final class HeraldRule extends HeraldDAO
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorIndexableInterface,
PhabricatorSubscribableInterface {
const TABLE_RULE_APPLIED = 'herald_ruleapplied';

View file

@ -0,0 +1,65 @@
<?php
final class HeraldRuleListView
extends AphrontView {
private $rules;
public function setRules(array $rules) {
assert_instances_of($rules, 'HeraldRule');
$this->rules = $rules;
return $this;
}
public function render() {
return $this->newObjectList();
}
public function newObjectList() {
$viewer = $this->getViewer();
$rules = $this->rules;
$handles = $viewer->loadHandles(mpull($rules, 'getAuthorPHID'));
$content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer);
$list = id(new PHUIObjectItemListView())
->setViewer($viewer);
foreach ($rules as $rule) {
$monogram = $rule->getMonogram();
$item = id(new PHUIObjectItemView())
->setObjectName($monogram)
->setHeader($rule->getName())
->setHref($rule->getURI());
if ($rule->isPersonalRule()) {
$item->addIcon('fa-user', pht('Personal Rule'));
$item->addByline(
pht(
'Authored by %s',
$handles[$rule->getAuthorPHID()]->renderLink()));
} else if ($rule->isObjectRule()) {
$item->addIcon('fa-briefcase', pht('Object Rule'));
} else {
$item->addIcon('fa-globe', pht('Global Rule'));
}
if ($rule->getIsDisabled()) {
$item->setDisabled(true);
$item->addIcon('fa-lock grey', pht('Disabled'));
} else if (!$rule->hasValidAuthor()) {
$item->setDisabled(true);
$item->addIcon('fa-user grey', pht('Author Not Active'));
}
$content_type_name = idx($content_type_map, $rule->getContentType());
$item->addAttribute(pht('Affects: %s', $content_type_name));
$list->addItem($item);
}
return $list;
}
}