1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-26 08:42:41 +01:00

Track how many columns use a particular trigger

Summary:
Ref T5474. In 99% of cases, a separate "archived/active" status for triggers probably doesn't make much sense: there's not much reason to ever disable/archive a trigger explcitly, and the archival rule is really just "is this trigger used by anything?".

(The one reason I can think of to disable a trigger manually is because you want to put something in a column and skip trigger rules, but you can already do this from the task detail page anyway, and disabling the trigger globally is a bad way to accomplish this if it's in use by other columns.)

Instead of adding a separate "status", just track how many columns a trigger is used by and consider it "inactive" if it is not used by any active columns.

Test Plan: This is slightly hard to test exhaustively since you can't share a trigger across multiple columns right now, but: rebuild indexes, poked around the trigger list and trigger details, added/removed triggers.

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T5474

Differential Revision: https://secure.phabricator.com/D20308
This commit is contained in:
epriestley 2019-03-22 07:30:02 -07:00
parent bfa5ffe8a1
commit 47856dc93f
12 changed files with 360 additions and 11 deletions

View file

@ -0,0 +1,8 @@
CREATE TABLE {$NAMESPACE}_project.project_triggerusage (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
triggerPHID VARBINARY(64) NOT NULL,
examplePHID VARBINARY(64),
columnCount INT UNSIGNED NOT NULL,
activeColumnCount INT UNSIGNED NOT NULL,
UNIQUE KEY `key_trigger` (triggerPHID)
) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};

View file

@ -4193,6 +4193,8 @@ phutil_register_library_map(array(
'PhabricatorProjectTriggerTransactionQuery' => 'applications/project/query/PhabricatorProjectTriggerTransactionQuery.php', 'PhabricatorProjectTriggerTransactionQuery' => 'applications/project/query/PhabricatorProjectTriggerTransactionQuery.php',
'PhabricatorProjectTriggerTransactionType' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php', 'PhabricatorProjectTriggerTransactionType' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php',
'PhabricatorProjectTriggerUnknownRule' => 'applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php', 'PhabricatorProjectTriggerUnknownRule' => 'applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php',
'PhabricatorProjectTriggerUsage' => 'applications/project/storage/PhabricatorProjectTriggerUsage.php',
'PhabricatorProjectTriggerUsageIndexEngineExtension' => 'applications/project/engineextension/PhabricatorProjectTriggerUsageIndexEngineExtension.php',
'PhabricatorProjectTriggerViewController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php', 'PhabricatorProjectTriggerViewController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php',
'PhabricatorProjectTypeTransaction' => 'applications/project/xaction/PhabricatorProjectTypeTransaction.php', 'PhabricatorProjectTypeTransaction' => 'applications/project/xaction/PhabricatorProjectTypeTransaction.php',
'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php', 'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php',
@ -10307,6 +10309,7 @@ phutil_register_library_map(array(
'PhabricatorProjectDAO', 'PhabricatorProjectDAO',
'PhabricatorApplicationTransactionInterface', 'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface', 'PhabricatorPolicyInterface',
'PhabricatorIndexableInterface',
'PhabricatorDestructibleInterface', 'PhabricatorDestructibleInterface',
), ),
'PhabricatorProjectTriggerController' => 'PhabricatorProjectController', 'PhabricatorProjectTriggerController' => 'PhabricatorProjectController',
@ -10328,6 +10331,8 @@ phutil_register_library_map(array(
'PhabricatorProjectTriggerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorProjectTriggerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorProjectTriggerTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorProjectTriggerTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorProjectTriggerUnknownRule' => 'PhabricatorProjectTriggerRule', 'PhabricatorProjectTriggerUnknownRule' => 'PhabricatorProjectTriggerRule',
'PhabricatorProjectTriggerUsage' => 'PhabricatorProjectDAO',
'PhabricatorProjectTriggerUsageIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'PhabricatorProjectTriggerViewController' => 'PhabricatorProjectTriggerController', 'PhabricatorProjectTriggerViewController' => 'PhabricatorProjectTriggerController',
'PhabricatorProjectTypeTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectTypeTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener', 'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener',

View file

@ -71,17 +71,23 @@ final class PhabricatorProjectTriggerViewController
// so we load only the columns they can actually see, but have a list of // so we load only the columns they can actually see, but have a list of
// all the impacted column PHIDs. // all the impacted column PHIDs.
// (We're also exposing the status of columns the user might not be able
// to see. This technically violates policy, but the trigger usage table
// hints at it anyway and it seems unlikely to ever have any security
// impact, but is useful in assessing whether a trigger is really in use
// or not.)
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); $omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
$all_columns = id(new PhabricatorProjectColumnQuery()) $all_columns = id(new PhabricatorProjectColumnQuery())
->setViewer($omnipotent_viewer) ->setViewer($omnipotent_viewer)
->withTriggerPHIDs(array($trigger->getPHID())) ->withTriggerPHIDs(array($trigger->getPHID()))
->execute(); ->execute();
$column_phids = mpull($all_columns, 'getPHID'); $column_map = mpull($all_columns, 'getStatus', 'getPHID');
if ($column_phids) { if ($column_map) {
$visible_columns = id(new PhabricatorProjectColumnQuery()) $visible_columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer) ->setViewer($viewer)
->withPHIDs($column_phids) ->withPHIDs(array_keys($column_map))
->execute(); ->execute();
$visible_columns = mpull($visible_columns, null, 'getPHID'); $visible_columns = mpull($visible_columns, null, 'getPHID');
} else { } else {
@ -89,7 +95,7 @@ final class PhabricatorProjectTriggerViewController
} }
$rows = array(); $rows = array();
foreach ($column_phids as $column_phid) { foreach ($column_map as $column_phid => $column_status) {
$column = idx($visible_columns, $column_phid); $column = idx($visible_columns, $column_phid);
if ($column) { if ($column) {
@ -113,7 +119,18 @@ final class PhabricatorProjectTriggerViewController
$column_name = phutil_tag('em', array(), pht('Restricted Column')); $column_name = phutil_tag('em', array(), pht('Restricted Column'));
} }
if ($column_status == PhabricatorProjectColumn::STATUS_ACTIVE) {
$status_icon = id(new PHUIIconView())
->setIcon('fa-columns', 'blue')
->setTooltip(pht('Active Column'));
} else {
$status_icon = id(new PHUIIconView())
->setIcon('fa-eye-slash', 'grey')
->setTooltip(pht('Hidden Column'));
}
$rows[] = array( $rows[] = array(
$status_icon,
$project_name, $project_name,
$column_name, $column_name,
); );
@ -123,11 +140,13 @@ final class PhabricatorProjectTriggerViewController
->setNoDataString(pht('This trigger is not used by any columns.')) ->setNoDataString(pht('This trigger is not used by any columns.'))
->setHeaders( ->setHeaders(
array( array(
null,
pht('Project'), pht('Project'),
pht('Column'), pht('Column'),
)) ))
->setColumnClasses( ->setColumnClasses(
array( array(
null,
null, null,
'wide pri', 'wide pri',
)); ));

View file

@ -27,4 +27,8 @@ final class PhabricatorProjectTriggerEditor
return $types; return $types;
} }
protected function supportsSearch() {
return true;
}
} }

View file

@ -0,0 +1,69 @@
<?php
final class PhabricatorProjectTriggerUsageIndexEngineExtension
extends PhabricatorIndexEngineExtension {
const EXTENSIONKEY = 'trigger.usage';
public function getExtensionName() {
return pht('Trigger Usage');
}
public function shouldIndexObject($object) {
if (!($object instanceof PhabricatorProjectTrigger)) {
return false;
}
return true;
}
public function indexObject(
PhabricatorIndexEngine $engine,
$object) {
$usage_table = new PhabricatorProjectTriggerUsage();
$column_table = new PhabricatorProjectColumn();
$conn_w = $object->establishConnection('w');
$active_statuses = array(
PhabricatorProjectColumn::STATUS_ACTIVE,
);
// Select summary information to populate the usage index. When picking
// an "examplePHID", we try to pick an active column.
$row = queryfx_one(
$conn_w,
'SELECT phid, COUNT(*) N, SUM(IF(status IN (%Ls), 1, 0)) M FROM %R
WHERE triggerPHID = %s
ORDER BY IF(status IN (%Ls), 1, 0) DESC, id ASC',
$active_statuses,
$column_table,
$object->getPHID(),
$active_statuses);
if ($row) {
$example_phid = $row['phid'];
$column_count = $row['N'];
$active_count = $row['M'];
} else {
$example_phid = null;
$column_count = 0;
$active_count = 0;
}
queryfx(
$conn_w,
'INSERT INTO %R (triggerPHID, examplePHID, columnCount, activeColumnCount)
VALUES (%s, %ns, %d, %d)
ON DUPLICATE KEY UPDATE
examplePHID = VALUES(examplePHID),
columnCount = VALUES(columnCount),
activeColumnCount = VALUES(activeColumnCount)',
$usage_table,
$object->getPHID(),
$example_phid,
$column_count,
$active_count);
}
}

View file

@ -5,6 +5,10 @@ final class PhabricatorProjectTriggerQuery
private $ids; private $ids;
private $phids; private $phids;
private $activeColumnMin;
private $activeColumnMax;
private $needUsage;
public function withIDs(array $ids) { public function withIDs(array $ids) {
$this->ids = $ids; $this->ids = $ids;
@ -16,6 +20,17 @@ final class PhabricatorProjectTriggerQuery
return $this; return $this;
} }
public function needUsage($need_usage) {
$this->needUsage = $need_usage;
return $this;
}
public function withActiveColumnCountBetween($min, $max) {
$this->activeColumnMin = $min;
$this->activeColumnMax = $max;
return $this;
}
public function newResultObject() { public function newResultObject() {
return new PhabricatorProjectTrigger(); return new PhabricatorProjectTrigger();
} }
@ -30,22 +45,91 @@ final class PhabricatorProjectTriggerQuery
if ($this->ids !== null) { if ($this->ids !== null) {
$where[] = qsprintf( $where[] = qsprintf(
$conn, $conn,
'id IN (%Ld)', 'trigger.id IN (%Ld)',
$this->ids); $this->ids);
} }
if ($this->phids !== null) { if ($this->phids !== null) {
$where[] = qsprintf( $where[] = qsprintf(
$conn, $conn,
'phid IN (%Ls)', 'trigger.phid IN (%Ls)',
$this->phids); $this->phids);
} }
if ($this->activeColumnMin !== null) {
$where[] = qsprintf(
$conn,
'trigger_usage.activeColumnCount >= %d',
$this->activeColumnMin);
}
if ($this->activeColumnMax !== null) {
$where[] = qsprintf(
$conn,
'trigger_usage.activeColumnCount <= %d',
$this->activeColumnMax);
}
return $where; return $where;
} }
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->shouldJoinUsageTable()) {
$joins[] = qsprintf(
$conn,
'JOIN %R trigger_usage ON trigger.phid = trigger_usage.triggerPHID',
new PhabricatorProjectTriggerUsage());
}
return $joins;
}
private function shouldJoinUsageTable() {
if ($this->activeColumnMin !== null) {
return true;
}
if ($this->activeColumnMax !== null) {
return true;
}
return false;
}
protected function didFilterPage(array $triggers) {
if ($this->needUsage) {
$usage_map = id(new PhabricatorProjectTriggerUsage())->loadAllWhere(
'triggerPHID IN (%Ls)',
mpull($triggers, 'getPHID'));
$usage_map = mpull($usage_map, null, 'getTriggerPHID');
foreach ($triggers as $trigger) {
$trigger_phid = $trigger->getPHID();
$usage = idx($usage_map, $trigger_phid);
if (!$usage) {
$usage = id(new PhabricatorProjectTriggerUsage())
->setTriggerPHID($trigger_phid)
->setExamplePHID(null)
->setColumnCount(0)
->setActiveColumnCount(0);
}
$trigger->attachUsage($usage);
}
}
return $triggers;
}
public function getQueryApplicationClass() { public function getQueryApplicationClass() {
return 'PhabricatorProjectApplication'; return 'PhabricatorProjectApplication';
} }
protected function getPrimaryTableAlias() {
return 'trigger';
}
} }

View file

@ -12,16 +12,33 @@ final class PhabricatorProjectTriggerSearchEngine
} }
public function newQuery() { public function newQuery() {
return new PhabricatorProjectTriggerQuery(); return id(new PhabricatorProjectTriggerQuery())
->needUsage(true);
} }
protected function buildCustomSearchFields() { protected function buildCustomSearchFields() {
return array(); return array(
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Active'))
->setKey('isActive')
->setOptions(
pht('(Show All)'),
pht('Show Only Active Triggers'),
pht('Show Only Inactive Triggers')),
);
} }
protected function buildQueryFromParameters(array $map) { protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery(); $query = $this->newQuery();
if ($map['isActive'] !== null) {
if ($map['isActive']) {
$query->withActiveColumnCountBetween(1, null);
} else {
$query->withActiveColumnCountBetween(null, 0);
}
}
return $query; return $query;
} }
@ -32,7 +49,8 @@ final class PhabricatorProjectTriggerSearchEngine
protected function getBuiltinQueryNames() { protected function getBuiltinQueryNames() {
$names = array(); $names = array();
$names['all'] = pht('All'); $names['active'] = pht('Active Triggers');
$names['all'] = pht('All Triggers');
return $names; return $names;
} }
@ -42,6 +60,8 @@ final class PhabricatorProjectTriggerSearchEngine
$query->setQueryKey($query_key); $query->setQueryKey($query_key);
switch ($query_key) { switch ($query_key) {
case 'active':
return $query->setParameter('isActive', true);
case 'all': case 'all':
return $query; return $query;
} }
@ -56,13 +76,73 @@ final class PhabricatorProjectTriggerSearchEngine
assert_instances_of($triggers, 'PhabricatorProjectTrigger'); assert_instances_of($triggers, 'PhabricatorProjectTrigger');
$viewer = $this->requireViewer(); $viewer = $this->requireViewer();
$example_phids = array();
foreach ($triggers as $trigger) {
$example_phid = $trigger->getUsage()->getExamplePHID();
if ($example_phid) {
$example_phids[] = $example_phid;
}
}
$handles = $viewer->loadHandles($example_phids);
$list = id(new PHUIObjectItemListView()) $list = id(new PHUIObjectItemListView())
->setViewer($viewer); ->setViewer($viewer);
foreach ($triggers as $trigger) { foreach ($triggers as $trigger) {
$usage = $trigger->getUsage();
$column_handle = null;
$have_column = false;
$example_phid = $usage->getExamplePHID();
if ($example_phid) {
$column_handle = $handles[$example_phid];
if ($column_handle->isComplete()) {
if (!$column_handle->getPolicyFiltered()) {
$have_column = true;
}
}
}
$column_count = $usage->getColumnCount();
$active_count = $usage->getActiveColumnCount();
if ($have_column) {
if ($active_count > 1) {
$usage_description = pht(
'Used on %s and %s other active column(s).',
$column_handle->renderLink(),
new PhutilNumber($active_count - 1));
} else if ($column_count > 1) {
$usage_description = pht(
'Used on %s and %s other column(s).',
$column_handle->renderLink(),
new PhutilNumber($column_count - 1));
} else {
$usage_description = pht(
'Used on %s.',
$column_handle->renderLink());
}
} else {
if ($active_count) {
$usage_description = pht(
'Used on %s active column(s).',
new PhutilNumber($active_count));
} else if ($column_count) {
$usage_description = pht(
'Used on %s column(s).',
new PhutilNumber($column_count));
} else {
$usage_description = pht(
'Unused trigger.');
}
}
$item = id(new PHUIObjectItemView()) $item = id(new PHUIObjectItemView())
->setObjectName($trigger->getObjectName()) ->setObjectName($trigger->getObjectName())
->setHeader($trigger->getDisplayName()) ->setHeader($trigger->getDisplayName())
->setHref($trigger->getURI()); ->setHref($trigger->getURI())
->addAttribute($usage_description)
->setDisabled(!$active_count);
$list->addItem($item); $list->addItem($item);
} }

View file

@ -5,6 +5,7 @@ final class PhabricatorProjectTrigger
implements implements
PhabricatorApplicationTransactionInterface, PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface, PhabricatorPolicyInterface,
PhabricatorIndexableInterface,
PhabricatorDestructibleInterface { PhabricatorDestructibleInterface {
protected $name; protected $name;
@ -12,6 +13,7 @@ final class PhabricatorProjectTrigger
protected $editPolicy; protected $editPolicy;
private $triggerRules; private $triggerRules;
private $usage = self::ATTACHABLE;
public static function initializeNewTrigger() { public static function initializeNewTrigger() {
$default_edit = PhabricatorPolicies::POLICY_USER; $default_edit = PhabricatorPolicies::POLICY_USER;
@ -257,6 +259,15 @@ final class PhabricatorProjectTrigger
return $sounds; return $sounds;
} }
public function getUsage() {
return $this->assertAttached($this->usage);
}
public function attachUsage(PhabricatorProjectTriggerUsage $usage) {
$this->usage = $usage;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */ /* -( PhabricatorApplicationTransactionInterface )------------------------- */
@ -310,6 +321,13 @@ final class PhabricatorProjectTrigger
new PhabricatorProjectColumn(), new PhabricatorProjectColumn(),
$this->getPHID()); $this->getPHID());
// Remove the usage index row for this trigger, if one exists.
queryfx(
$conn,
'DELETE FROM %R WHERE triggerPHID = %s',
new PhabricatorProjectTriggerUsage(),
$this->getPHID());
$this->delete(); $this->delete();
$this->saveTransaction(); $this->saveTransaction();

View file

@ -0,0 +1,28 @@
<?php
final class PhabricatorProjectTriggerUsage
extends PhabricatorProjectDAO {
protected $triggerPHID;
protected $examplePHID;
protected $columnCount;
protected $activeColumnCount;
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
'examplePHID' => 'phid?',
'columnCount' => 'uint32',
'activeColumnCount' => 'uint32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_trigger' => array(
'columns' => array('triggerPHID'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
}

View file

@ -13,6 +13,15 @@ final class PhabricatorProjectColumnStatusTransaction
$object->setStatus($value); $object->setStatus($value);
} }
public function applyExternalEffects($object, $value) {
// Update the trigger usage index, which cares about whether columns are
// active or not.
$trigger_phid = $object->getTriggerPHID();
if ($trigger_phid) {
PhabricatorSearchWorker::queueDocumentForIndexing($trigger_phid);
}
}
public function getTitle() { public function getTitle() {
$new = $this->getNewValue(); $new = $this->getNewValue();

View file

@ -13,6 +13,25 @@ final class PhabricatorProjectColumnTriggerTransaction
$object->setTriggerPHID($value); $object->setTriggerPHID($value);
} }
public function applyExternalEffects($object, $value) {
// After we change the trigger attached to a column, update the search
// indexes for the old and new triggers so we update the usage index.
$old = $this->getOldValue();
$new = $this->getNewValue();
$column_phids = array();
if ($old) {
$column_phids[] = $old;
}
if ($new) {
$column_phids[] = $new;
}
foreach ($column_phids as $phid) {
PhabricatorSearchWorker::queueDocumentForIndexing($phid);
}
}
public function getTitle() { public function getTitle() {
$old = $this->getOldValue(); $old = $this->getOldValue();
$new = $this->getNewValue(); $new = $this->getNewValue();

View file

@ -136,7 +136,13 @@ final class PhabricatorSearchManagementIndexWorkflow
if ($track_skips) { if ($track_skips) {
$new_versions = $this->loadIndexVersions($phid); $new_versions = $this->loadIndexVersions($phid);
if ($old_versions !== $new_versions) {
if (!$old_versions && !$new_versions) {
// If the document doesn't use an index version, both the lists
// of versions will be empty. We still rebuild the index in this
// case.
$count_updated++;
} else if ($old_versions !== $new_versions) {
$count_updated++; $count_updated++;
} else { } else {
$count_skipped++; $count_skipped++;