1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-28 08:20:57 +01:00

Allow Herald rules to be disabled, instead of deleted

Summary:
Ref T603. Ref T1279. Further improves transaction and policy support for Herald.

  - Instead of deleting rules (which wipes out history and can't be undone) allow them to be disabled.
  - Track disables with transactions.
  - Gate disables with policy controls.
  - Show policy and status information in the headers.
  - Show transaction history on rule detail screens.
  - Remove the delete controller.
  - Support disabled queries in the ApplicationSearch.

Test Plan:
  - Enabled and disabled rules.
  - Searched for enabled/disabled rules.
  - Verified disabled rules don't activate.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T1279, T603

Differential Revision: https://secure.phabricator.com/D7247
This commit is contained in:
epriestley 2013-10-06 17:10:29 -07:00
parent c6add6ae73
commit 953ff197bf
20 changed files with 408 additions and 96 deletions

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_herald.herald_rule
ADD isDisabled INT UNSIGNED NOT NULL DEFAULT 0;

View file

@ -631,8 +631,8 @@ phutil_register_library_map(array(
'HeraldConditionTranscript' => 'applications/herald/storage/transcript/HeraldConditionTranscript.php',
'HeraldController' => 'applications/herald/controller/HeraldController.php',
'HeraldDAO' => 'applications/herald/storage/HeraldDAO.php',
'HeraldDeleteController' => 'applications/herald/controller/HeraldDeleteController.php',
'HeraldDifferentialRevisionAdapter' => 'applications/herald/adapter/HeraldDifferentialRevisionAdapter.php',
'HeraldDisableController' => 'applications/herald/controller/HeraldDisableController.php',
'HeraldEditLogQuery' => 'applications/herald/query/HeraldEditLogQuery.php',
'HeraldEffect' => 'applications/herald/engine/HeraldEffect.php',
'HeraldEngine' => 'applications/herald/engine/HeraldEngine.php',
@ -651,6 +651,7 @@ phutil_register_library_map(array(
'HeraldRuleEdit' => 'applications/herald/storage/HeraldRuleEdit.php',
'HeraldRuleEditHistoryController' => 'applications/herald/controller/HeraldRuleEditHistoryController.php',
'HeraldRuleEditHistoryView' => 'applications/herald/view/HeraldRuleEditHistoryView.php',
'HeraldRuleEditor' => 'applications/herald/editor/HeraldRuleEditor.php',
'HeraldRuleListController' => 'applications/herald/controller/HeraldRuleListController.php',
'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php',
'HeraldRuleSearchEngine' => 'applications/herald/query/HeraldRuleSearchEngine.php',
@ -660,6 +661,7 @@ phutil_register_library_map(array(
'HeraldRuleTypeConfig' => 'applications/herald/config/HeraldRuleTypeConfig.php',
'HeraldRuleViewController' => 'applications/herald/controller/HeraldRuleViewController.php',
'HeraldTestConsoleController' => 'applications/herald/controller/HeraldTestConsoleController.php',
'HeraldTransactionQuery' => 'applications/herald/query/HeraldTransactionQuery.php',
'HeraldTranscript' => 'applications/herald/storage/transcript/HeraldTranscript.php',
'HeraldTranscriptController' => 'applications/herald/controller/HeraldTranscriptController.php',
'HeraldTranscriptListController' => 'applications/herald/controller/HeraldTranscriptListController.php',
@ -2724,8 +2726,8 @@ phutil_register_library_map(array(
'HeraldCondition' => 'HeraldDAO',
'HeraldController' => 'PhabricatorController',
'HeraldDAO' => 'PhabricatorLiskDAO',
'HeraldDeleteController' => 'HeraldController',
'HeraldDifferentialRevisionAdapter' => 'HeraldAdapter',
'HeraldDisableController' => 'HeraldController',
'HeraldEditLogQuery' => 'PhabricatorOffsetPagedQuery',
'HeraldInvalidActionException' => 'Exception',
'HeraldInvalidConditionException' => 'Exception',
@ -2744,6 +2746,7 @@ phutil_register_library_map(array(
'HeraldRuleEdit' => 'HeraldDAO',
'HeraldRuleEditHistoryController' => 'HeraldController',
'HeraldRuleEditHistoryView' => 'AphrontView',
'HeraldRuleEditor' => 'PhabricatorApplicationTransactionEditor',
'HeraldRuleListController' =>
array(
0 => 'HeraldController',
@ -2755,6 +2758,7 @@ phutil_register_library_map(array(
'HeraldRuleTransactionComment' => 'PhabricatorApplicationTransactionComment',
'HeraldRuleViewController' => 'HeraldController',
'HeraldTestConsoleController' => 'HeraldController',
'HeraldTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HeraldTranscript' =>
array(
0 => 'HeraldDAO',

View file

@ -41,8 +41,9 @@ final class PhabricatorApplicationHerald extends PhabricatorApplication {
=> 'HeraldNewController',
'rule/(?P<id>[1-9]\d*)/' => 'HeraldRuleViewController',
'edit/(?:(?P<id>[1-9]\d*)/)?' => 'HeraldRuleController',
'disable/(?P<id>[1-9]\d*)/(?P<action>\w+)/' =>
'HeraldDisableController',
'history/(?:(?P<id>[1-9]\d*)/)?' => 'HeraldRuleEditHistoryController',
'delete/(?P<id>[1-9]\d*)/' => 'HeraldDeleteController',
'test/' => 'HeraldTestConsoleController',
'transcript/' => 'HeraldTranscriptListController',
'transcript/(?P<id>[1-9]\d*)/(?:(?P<filter>\w+)/)?'

View file

@ -1,57 +0,0 @@
<?php
final class HeraldDeleteController extends HeraldController {
private $id;
public function getFilter() {
// note this controller is only used from a dialog-context at the moment
// and there is actually no "delete" filter
return 'delete';
}
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$rule = id(new HeraldRule())->load($this->id);
if (!$rule) {
return new Aphront404Response();
}
$request = $this->getRequest();
$user = $request->getUser();
// Anyone can delete a global rule, but only the rule owner can delete a
// personal one.
if ($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
if ($user->getPHID() != $rule->getAuthorPHID()) {
return new Aphront400Response();
}
}
if ($request->isFormPost()) {
$rule->openTransaction();
$rule->logEdit($user->getPHID(), 'delete');
$rule->delete();
$rule->saveTransaction();
return id(new AphrontReloadResponse())->setURI('/herald/');
}
$dialog = new AphrontDialogView();
$dialog->setUser($request->getUser());
$dialog->setTitle(pht('Really delete this rule?'));
$dialog->appendChild(pht(
"Are you sure you want to delete the rule: %s?",
$rule->getName()));
$dialog->addSubmitButton(pht('Delete'));
$dialog->addCancelButton('/herald/');
$dialog->setSubmitURI($request->getPath());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}

View file

@ -0,0 +1,74 @@
<?php
final class HeraldDisableController extends HeraldController {
private $id;
private $action;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$this->action = $data['action'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $this->id;
$rule = id(new HeraldRuleQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$rule) {
return new Aphront404Response();
}
if ($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) {
$this->requireApplicationCapability(
PhabricatorApplicationHerald::CAN_CREATE_GLOBAL_RULE);
}
$view_uri = $this->getApplicationURI("rule/{$id}/");
$is_disable = ($this->action === 'disable');
if ($request->isFormPost()) {
$xaction = id(new HeraldRuleTransaction())
->setTransactionType(HeraldRuleTransaction::TYPE_DISABLE)
->setNewValue($is_disable);
id(new HeraldRuleEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->applyTransactions($rule, array($xaction));
return id(new AphrontRedirectResponse())->setURI($view_uri);
}
if ($is_disable) {
$title = pht('Really disable this rule?');
$body = pht('This rule will no longer activate.');
$button = pht('Disable Rule');
} else {
$title = pht('Really enable this rule?');
$body = pht('This rule will become active again.');
$button = pht('Enable Rule');
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle($title)
->appendChild($body)
->addSubmitButton($button)
->addCancelButton($view_uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}

View file

@ -54,18 +54,17 @@ final class HeraldRuleListController extends HeraldController
$item->addIcon('world', pht('Global Rule'));
}
if ($rule->getIsDisabled()) {
$item->setDisabled(true);
$item->addIcon('disable-grey', pht('Disabled'));
}
$item->addAction(
id(new PHUIListItemView())
->setHref($this->getApplicationURI("history/{$id}/"))
->setIcon('transcript')
->setName(pht('Edit Log')));
$item->addAction(
id(new PHUIListItemView())
->setHref('/herald/delete/'.$rule->getID().'/')
->setIcon('delete')
->setWorkflow(true));
$content_type_name = idx($content_type_map, $rule->getContentType());
$item->addAttribute(pht('Affects: %s', $content_type_name));

View file

@ -22,7 +22,21 @@ final class HeraldRuleViewController extends HeraldController {
}
$header = id(new PHUIHeaderView())
->setHeader($rule->getName());
->setUser($viewer)
->setHeader($rule->getName())
->setPolicyObject($rule);
if ($rule->getIsDisabled()) {
$header->setStatus(
'oh-open',
'red',
pht('Disabled'));
} else {
$header->setStatus(
'oh-open',
null,
pht('Active'));
}
$actions = $this->buildActionView($rule);
$properties = $this->buildPropertyView($rule);
@ -37,10 +51,13 @@ final class HeraldRuleViewController extends HeraldController {
->setActionList($actions)
->setPropertyList($properties);
$timeline = $this->buildTimeline($rule);
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
$timeline,
),
array(
'title' => $rule->getName(),
@ -70,6 +87,25 @@ final class HeraldRuleViewController extends HeraldController {
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
if ($rule->getIsDisabled()) {
$disable_uri = "disable/{$id}/enable/";
$disable_icon = 'enable';
$disable_name = pht('Enable Rule');
} else {
$disable_uri = "disable/{$id}/disable/";
$disable_icon = 'disable';
$disable_name = pht('Disable Rule');
}
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Disable Rule'))
->setHref($this->getApplicationURI($disable_uri))
->setIcon($disable_icon)
->setName($disable_name)
->setDisabled(!$can_edit)
->setWorkflow(true));
return $view;
}
@ -115,4 +151,31 @@ final class HeraldRuleViewController extends HeraldController {
return $view;
}
private function buildTimeline(HeraldRule $rule) {
$viewer = $this->getRequest()->getUser();
$xactions = id(new HeraldTransactionQuery())
->setViewer($viewer)
->withObjectPHIDs(array($rule->getPHID()))
->needComments(true)
->execute();
$engine = id(new PhabricatorMarkupEngine())
->setViewer($viewer);
foreach ($xactions as $xaction) {
if ($xaction->getComment()) {
$engine->addObject(
$xaction->getComment(),
PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
}
}
$engine->process();
return id(new PhabricatorApplicationTransactionView())
->setUser($viewer)
->setObjectPHID($rule->getPHID())
->setTransactions($xactions)
->setMarkupEngine($engine);
}
}

View file

@ -59,6 +59,7 @@ final class HeraldTestConsoleController extends HeraldController {
$rules = id(new HeraldRuleQuery())
->setViewer($user)
->withContentTypes(array($adapter->getAdapterContentType()))
->withDisabled(false)
->needConditionsAndActions(true)
->needAppliedToPHIDs(array($object->getPHID()))
->needValidateAuthors(true)

View file

@ -0,0 +1,54 @@
<?php
final class HeraldRuleEditor
extends PhabricatorApplicationTransactionEditor {
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = HeraldRuleTransaction::TYPE_DISABLE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case HeraldRuleTransaction::TYPE_DISABLE:
return (int)$object->getIsDisabled();
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case HeraldRuleTransaction::TYPE_DISABLE:
return (int)$xaction->getNewValue();
}
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case HeraldRuleTransaction::TYPE_DISABLE:
return $object->setIsDisabled($xaction->getNewValue());
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return;
}
}

View file

@ -27,6 +27,7 @@ final class HeraldEngine {
public static function loadAndApplyRules(HeraldAdapter $adapter) {
$rules = id(new HeraldRuleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withDisabled(false)
->withContentTypes(array($adapter->getAdapterContentType()))
->needConditionsAndActions(true)
->needAppliedToPHIDs(array($adapter->getPHID()))

View file

@ -8,6 +8,7 @@ final class HeraldRuleQuery
private $authorPHIDs;
private $ruleTypes;
private $contentTypes;
private $disabled;
private $needConditionsAndActions;
private $needAppliedToPHIDs;
@ -43,6 +44,11 @@ final class HeraldRuleQuery
return $this;
}
public function withDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function needConditionsAndActions($need) {
$this->needConditionsAndActions = $need;
return $this;
@ -82,6 +88,7 @@ final class HeraldRuleQuery
$types = HeraldAdapter::getEnabledAdapterMap($this->getViewer());
foreach ($rules as $key => $rule) {
if (empty($types[$rule->getContentType()])) {
$this->didRejectResult($rule);
unset($rules[$key]);
}
}
@ -171,6 +178,13 @@ final class HeraldRuleQuery
$this->contentTypes);
}
if ($this->disabled !== null) {
$where[] = qsprintf(
$conn_r,
'rule.isDisabled = %d',
(int)$this->disabled);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);

View file

@ -12,6 +12,9 @@ final class HeraldRuleSearchEngine
$saved->setParameter('contentType', $request->getStr('contentType'));
$saved->setParameter('ruleType', $request->getStr('ruleType'));
$saved->setParameter(
'disabled',
$this->readBoolFromRequest($request, 'disabled'));
return $saved;
}
@ -36,6 +39,11 @@ final class HeraldRuleSearchEngine
$query->withRuleTypes(array($rule_type));
}
$disabled = $saved->getParameter('disabled');
if ($disabled !== null) {
$query->withDisabled($disabled);
}
return $query;
}
@ -71,7 +79,18 @@ final class HeraldRuleSearchEngine
->setName('ruleType')
->setLabel(pht('Rule Type'))
->setValue($rule_type)
->setOptions($this->getRuleTypeOptions()));
->setOptions($this->getRuleTypeOptions()))
->appendChild(
id(new AphrontFormSelectControl())
->setName('disabled')
->setLabel(pht('Rule Status'))
->setValue($this->getBoolFromQuery($saved_query, 'disabled'))
->setOptions(
array(
'' => pht('Show Enabled and Disabled Rules'),
'false' => pht('Show Only Enabled Rules'),
'true' => pht('Show Only Disabled Rules'),
)));
}
protected function getURI($path) {
@ -85,6 +104,7 @@ final class HeraldRuleSearchEngine
$names['authored'] = pht('Authored');
}
$names['active'] = pht('Active');
$names['all'] = pht('All');
return $names;
@ -95,13 +115,17 @@ final class HeraldRuleSearchEngine
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
$viewer_phid = $this->requireViewer()->getPHID();
switch ($query_key) {
case 'all':
return $query;
case 'active':
return $query->setParameter('disabled', false);
case 'authored':
return $query->setParameter(
'authorPHIDs',
array($this->requireViewer()->getPHID()));
return $query
->setParameter('authorPHIDs', array($viewer_phid))
->setParameter('disabled', false);
}
return parent::buildSavedQueryFromBuiltin($query_key);

View file

@ -0,0 +1,10 @@
<?php
final class HeraldTransactionQuery
extends PhabricatorApplicationTransactionQuery {
public function getTemplateApplicationTransaction() {
return new HeraldRuleTransaction();
}
}

View file

@ -12,6 +12,7 @@ final class HeraldRule extends HeraldDAO
protected $mustMatchAll;
protected $repetitionPolicy;
protected $ruleType;
protected $isDisabled = 0;
protected $configVersion = 14;
@ -198,7 +199,15 @@ final class HeraldRule extends HeraldDAO
public function getPolicy($capability) {
if ($this->isGlobalRule()) {
return PhabricatorPolicies::POLICY_USER;
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_USER;
case PhabricatorPolicyCapability::CAN_EDIT:
$app = 'PhabricatorApplicationHerald';
$herald = PhabricatorApplication::getByClass($app);
$global = PhabricatorApplicationHerald::CAN_CREATE_GLOBAL_RULE;
return $herald->getPolicy($global);
}
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
@ -213,9 +222,11 @@ final class HeraldRule extends HeraldDAO
}
public function describeAutomaticCapability($capability) {
// TODO: (T603) Sort this out.
if ($this->isPersonalRule()) {
return pht("A personal rule's owner can always view and edit it.");
}
return null;
}
}

View file

@ -4,6 +4,7 @@ final class HeraldRuleTransaction
extends PhabricatorApplicationTransaction {
const TYPE_EDIT = 'herald:edit';
const TYPE_DISABLE = 'herald:disable';
public function getApplicationName() {
return 'herald';
@ -17,5 +18,75 @@ final class HeraldRuleTransaction
return new HeraldRuleTransactionComment();
}
}
public function getColor() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_DISABLE:
if ($new) {
return 'red';
} else {
return 'green';
}
}
return parent::getColor();
}
public function getActionName() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_DISABLE:
if ($new) {
return pht('Disabled');
} else {
return pht('Enabled');
}
}
return parent::getActionName();
}
public function getIcon() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_DISABLE:
if ($new) {
return 'disable';
} else {
return 'enable';
}
}
return parent::getIcon();
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_DISABLE:
if ($new) {
return pht(
'%s disabled this rule.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s enabled this rule.',
$this->renderHandleLink($author_phid));
}
}
return parent::getTitle();
}
}

View file

@ -12,9 +12,15 @@ final class PhabricatorAppSearchEngine
$saved->setParameter('name', $request->getStr('name'));
$saved->setParameter('installed', $this->readBool($request, 'installed'));
$saved->setParameter('beta', $this->readBool($request, 'beta'));
$saved->setParameter('firstParty', $this->readBool($request, 'firstParty'));
$saved->setParameter(
'installed',
$this->readBoolFromRequest($request, 'installed'));
$saved->setParameter(
'beta',
$this->readBoolFromRequest($request, 'beta'));
$saved->setParameter(
'firstParty',
$this->readBoolFromRequest($request, 'firstParty'));
return $saved;
}
@ -61,7 +67,7 @@ final class PhabricatorAppSearchEngine
id(new AphrontFormSelectControl())
->setLabel(pht('Installed'))
->setName('installed')
->setValue($this->getBool($saved, 'installed'))
->setValue($this->getBoolFromQuery($saved, 'installed'))
->setOptions(
array(
'' => pht('Show All Applications'),
@ -72,7 +78,7 @@ final class PhabricatorAppSearchEngine
id(new AphrontFormSelectControl())
->setLabel(pht('Beta'))
->setName('beta')
->setValue($this->getBool($saved, 'beta'))
->setValue($this->getBoolFromQuery($saved, 'beta'))
->setOptions(
array(
'' => pht('Show All Applications'),
@ -83,7 +89,7 @@ final class PhabricatorAppSearchEngine
id(new AphrontFormSelectControl())
->setLabel(pht('Provenance'))
->setName('firstParty')
->setValue($this->getBool($saved, 'firstParty'))
->setValue($this->getBoolFromQuery($saved, 'firstParty'))
->setOptions(
array(
'' => pht('Show All Applications'),
@ -118,19 +124,4 @@ final class PhabricatorAppSearchEngine
return parent::buildSavedQueryFromBuiltin($query_key);
}
private function readBool(AphrontRequest $request, $key) {
if (!strlen($request->getStr($key))) {
return null;
}
return $request->getBool($key);
}
private function getBool(PhabricatorSavedQuery $query, $key) {
$value = $query->getParameter($key);
if ($value === null) {
return $value;
}
return $value ? 'true' : 'false';
}
}

View file

@ -29,6 +29,7 @@ final class PhabricatorRepositoryCommitHeraldWorker
$rules = id(new HeraldRuleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withContentTypes(array($adapter->getAdapterContentType()))
->withDisabled(false)
->needConditionsAndActions(true)
->needAppliedToPHIDs(array($adapter->getPHID()))
->needValidateAuthors(true)

View file

@ -299,6 +299,25 @@ abstract class PhabricatorApplicationSearchEngine {
}
protected function readBoolFromRequest(
AphrontRequest $request,
$key) {
if (!strlen($request->getStr($key))) {
return null;
}
return $request->getBool($key);
}
protected function getBoolFromQuery(PhabricatorSavedQuery $query, $key) {
$value = $query->getParameter($key);
if ($value === null) {
return $value;
}
return $value ? 'true' : 'false';
}
/* -( Dates )-------------------------------------------------------------- */

View file

@ -1660,6 +1660,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'php',
'name' => $this->getPatchPath('20131004.dxreviewers.php'),
),
'20131006.hdisable.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('20131006.hdisable.sql'),
),
);
}
}

View file

@ -65,6 +65,31 @@ final class PHUIHeaderView extends AphrontView {
return $this;
}
public function setStatus($icon, $color, $name) {
$header_class = 'phui-header-status';
if ($color) {
$icon = $icon.'-'.$color;
$header_class = $header_class.'-'.$color;
}
$img = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_STATUS)
->setSpriteIcon($icon);
$tag = phutil_tag(
'span',
array(
'class' => "{$header_class} plr",
),
array(
$img,
$name,
));
return $this->addProperty(self::PROPERTY_STATUS, $tag);
}
public function render() {
require_celerity_resource('phui-header-view-css');