1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

Allow Almanac service types to define default properties

Summary:
Ref T5833. This allows Almanac ServiceTypes to define default properties for a service, which show up in the UI and are more easily editable.

Overall, this makes it much easier to make structured/usable/consistent service records: you can check a checkbox that says "prevent new allocations" instead of needing to know the meaning of a key.

Test Plan: {F251593}

Reviewers: chad, btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T5833

Differential Revision: https://secure.phabricator.com/D10996
This commit is contained in:
epriestley 2014-12-17 11:10:50 -08:00
parent c85327ca3e
commit 3fa519da74
19 changed files with 361 additions and 77 deletions

View file

@ -34,6 +34,7 @@ return array(
'rsrc/css/aphront/transaction.css' => '5d0cae25',
'rsrc/css/aphront/two-column.css' => '16ab3ad2',
'rsrc/css/aphront/typeahead.css' => 'a989b5b3',
'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af',
'rsrc/css/application/auth/auth.css' => '1e655982',
'rsrc/css/application/base/main-menu-view.css' => '33e5f2f6',
'rsrc/css/application/base/notification-menu.css' => '6aa0a74b',
@ -497,6 +498,7 @@ return array(
'rsrc/swf/aphlict.swf' => 'f19daffb',
),
'symbols' => array(
'almanac-css' => 'dbb9b3af',
'aphront-bars' => '231ac33c',
'aphront-contextbar-view-css' => '1c3b0529',
'aphront-dark-console-css' => '6378ef3d',

View file

@ -67,6 +67,7 @@ phutil_register_library_map(array(
'AlmanacNetworkViewController' => 'applications/almanac/controller/AlmanacNetworkViewController.php',
'AlmanacProperty' => 'applications/almanac/storage/AlmanacProperty.php',
'AlmanacPropertyController' => 'applications/almanac/controller/AlmanacPropertyController.php',
'AlmanacPropertyDeleteController' => 'applications/almanac/controller/AlmanacPropertyDeleteController.php',
'AlmanacPropertyEditController' => 'applications/almanac/controller/AlmanacPropertyEditController.php',
'AlmanacPropertyInterface' => 'applications/almanac/property/AlmanacPropertyInterface.php',
'AlmanacPropertyQuery' => 'applications/almanac/query/AlmanacPropertyQuery.php',
@ -3092,6 +3093,7 @@ phutil_register_library_map(array(
'PhabricatorPolicyInterface',
),
'AlmanacPropertyController' => 'AlmanacController',
'AlmanacPropertyDeleteController' => 'AlmanacDeviceController',
'AlmanacPropertyEditController' => 'AlmanacDeviceController',
'AlmanacPropertyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'AlmanacQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',

View file

@ -57,7 +57,8 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication {
'(?P<id>\d+)/' => 'AlmanacNetworkViewController',
),
'property/' => array(
'edit/(?:(?P<id>\d+)/)?' => 'AlmanacPropertyEditController',
'edit/' => 'AlmanacPropertyEditController',
'delete/' => 'AlmanacPropertyDeleteController',
),
),
);

View file

@ -9,13 +9,129 @@ abstract class AlmanacController
$viewer = $this->getViewer();
$properties = $object->getAlmanacProperties();
$this->requireResource('almanac-css');
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
$field_list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_DEFAULT);
// Before reading values from the object, read defaults.
$defaults = mpull(
$field_list->getFields(),
'getValueForStorage',
'getFieldKey');
$field_list
->setViewer($viewer)
->readFieldsFromStorage($object);
Javelin::initBehavior('phabricator-tooltips', array());
$icon_builtin = id(new PHUIIconView())
->setIconFont('fa-circle')
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => pht('Builtin Property'),
'align' => 'E',
));
$icon_custom = id(new PHUIIconView())
->setIconFont('fa-circle-o grey')
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => pht('Custom Property'),
'align' => 'E',
));
$builtins = $object->getAlmanacPropertyFieldSpecifications();
// Sort fields so builtin fields appear first, then fields are ordered
// alphabetically.
$fields = $field_list->getFields();
$fields = msort($fields, 'getFieldKey');
$head = array();
$tail = array();
foreach ($fields as $field) {
$key = $field->getFieldKey();
if (isset($builtins[$key])) {
$head[$key] = $field;
} else {
$tail[$key] = $field;
}
}
$fields = $head + $tail;
$rows = array();
foreach ($properties as $property) {
$value = $property->getFieldValue();
foreach ($fields as $key => $field) {
$value = $field->getValueForStorage();
$is_builtin = isset($builtins[$key]);
$delete_uri = $this->getApplicationURI('property/delete/');
$delete_uri = id(new PhutilURI($delete_uri))
->setQueryParams(
array(
'objectPHID' => $object->getPHID(),
'key' => $key,
));
$edit_uri = $this->getApplicationURI('property/edit/');
$edit_uri = id(new PhutilURI($edit_uri))
->setQueryParams(
array(
'objectPHID' => $object->getPHID(),
'key' => $key,
));
$delete = javelin_tag(
'a',
array(
'class' => ($can_edit
? 'button grey small'
: 'button grey small disabled'),
'sigil' => 'workflow',
'href' => $delete_uri,
),
$is_builtin ? pht('Reset') : pht('Delete'));
$default = idx($defaults, $key);
$is_default = ($default !== null && $default === $value);
$display_value = PhabricatorConfigJSON::prettyPrintJSON($value);
if ($is_default) {
$display_value = phutil_tag(
'span',
array(
'class' => 'almanac-default-property-value',
),
$display_value);
}
$display_key = $key;
if ($can_edit) {
$display_key = javelin_tag(
'a',
array(
'href' => $edit_uri,
'sigil' => 'workflow',
),
$display_key);
}
$rows[] = array(
$property->getFieldName(),
PhabricatorConfigJSON::prettyPrintJSON($value),
($is_builtin ? $icon_builtin : $icon_custom),
$display_key,
$display_value,
$delete,
);
}
@ -23,13 +139,17 @@ abstract class AlmanacController
->setNoDataString(pht('No properties.'))
->setHeaders(
array(
null,
pht('Name'),
pht('Value'),
null,
))
->setColumnClasses(
array(
null,
null,
'wide',
'action',
));
$phid = $object->getPHID();

View file

@ -11,7 +11,6 @@ final class AlmanacNetworkViewController
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$network = id(new AlmanacNetworkQuery())
->setViewer($viewer)
->withIDs(array($id))

View file

@ -0,0 +1,109 @@
<?php
final class AlmanacPropertyDeleteController
extends AlmanacDeviceController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($request->getStr('objectPHID')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
return new Aphront404Response();
}
if (!($object instanceof AlmanacPropertyInterface)) {
return new Aphront404Response();
}
$key = $request->getStr('key');
if (!strlen($key)) {
return new Aphront404Response();
}
$cancel_uri = $object->getURI();
$builtins = $object->getAlmanacPropertyFieldSpecifications();
$is_builtin = isset($builtins[$key]);
if ($is_builtin) {
// This is a builtin property, so we're going to reset it to the
// default value.
$field_list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_DEFAULT);
// Note that we're NOT loading field values from the object: we just want
// to get the field's default value so we can reset it.
$fields = $field_list->getFields();
$field = $fields[$key];
$is_delete = false;
$new_value = $field->getValueForStorage();
// Now, load the field to get the old value.
$field_list
->setViewer($viewer)
->readFieldsFromStorage($object);
$old_value = $field->getValueForStorage();
$title = pht('Reset Property');
$body = pht('Reset this property to its default value?');
$submit_text = pht('Reset');
} else {
// This is a custom property, so we're going to delete it outright.
$is_delete = true;
$old_value = $object->getAlmanacPropertyValue($key);
$new_value = null;
$title = pht('Delete Property');
$body = pht('Delete this property? TODO: DOES NOT WORK YET');
$submit_text = pht('Delete');
}
$validation_exception = null;
if ($request->isFormPost()) {
$xaction = $object->getApplicationTransactionTemplate()
->setTransactionType(PhabricatorTransactions::TYPE_CUSTOMFIELD)
->setMetadataValue('customfield:key', $key)
->setOldValue($old_value)
->setNewValue($new_value);
// TODO: We aren't really deleting properties that we claim to delete
// yet, but that needs to be specialized a little bit.
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
try {
$editor->applyTransactions($object, array($xaction));
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
}
}
return $this->newDialog()
->setTitle($title)
->setValidationException($validation_exception)
->addHiddenInput('objectPHID', $object->getPHID())
->addHiddenInput('key', $key)
->appendParagraph($body)
->addCancelButton($cancel_uri)
->addSubmitButton($submit_text);
}
}

View file

@ -6,43 +6,17 @@ final class AlmanacPropertyEditController
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
if ($id) {
$property = id(new AlmanacPropertyQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$property) {
return new Aphront404Response();
}
$object = $property->getObject();
$is_new = false;
$title = pht('Edit Property');
$save_button = pht('Save Changes');
} else {
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($request->getStr('objectPHID')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
return new Aphront404Response();
}
$is_new = true;
$title = pht('Add Property');
$save_button = pht('Add Property');
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($request->getStr('objectPHID')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
return new Aphront404Response();
}
if (!($object instanceof AlmanacPropertyInterface)) {
@ -51,6 +25,21 @@ final class AlmanacPropertyEditController
$cancel_uri = $object->getURI();
$key = $request->getStr('key');
if ($key) {
$property_key = $key;
$is_new = false;
$title = pht('Edit Property');
$save_button = pht('Save Changes');
} else {
$property_key = null;
$is_new = true;
$title = pht('Add Property');
$save_button = pht('Add Property');
}
if ($is_new) {
$errors = array();
$property = null;
@ -77,25 +66,11 @@ final class AlmanacPropertyEditController
}
if (!$errors) {
$property = id(new AlmanacPropertyQuery())
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()))
->withNames(array($name))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$property) {
$property = id(new AlmanacProperty())
->setObjectPHID($object->getPHID())
->setFieldName($name);
}
$property_key = $name;
}
}
if (!$property) {
if ($property_key === null) {
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
@ -115,17 +90,29 @@ final class AlmanacPropertyEditController
}
}
$v_name = $property->getFieldName();
$e_name = true;
// Make sure property key is appropriate.
// TODO: It would be cleaner to put this safety check in the Editor.
AlmanacNames::validateServiceOrDeviceName($property_key);
$v_value = $property->getFieldValue();
$e_value = null;
// If we're adding a new property, put a placeholder on the object so
// that we can build a CustomField for it.
if (!$object->hasAlmanacProperty($property_key)) {
$temporary_property = id(new AlmanacProperty())
->setObjectPHID($object->getPHID())
->setFieldName($property_key);
$object->attachAlmanacProperties(array($property));
$object->attachAlmanacProperties(array($temporary_property));
}
$field_list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_EDIT);
PhabricatorCustomField::ROLE_DEFAULT);
// Select only the field being edited.
$fields = $field_list->getFields();
$fields = array_select_keys($fields, array($property_key));
$field_list = new PhabricatorCustomFieldList($fields);
$field_list
->setViewer($viewer)
->readFieldsFromStorage($object);
@ -153,7 +140,8 @@ final class AlmanacPropertyEditController
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('objectPHID', $request->getStr('objectPHID'))
->addHiddenInput('name', $request->getStr('name'))
->addHiddenInput('key', $request->getStr('key'))
->addHiddenInput('name', $property_key)
->addHiddenInput('isValueEdit', true);
$field_list->appendFieldsToForm($form);

View file

@ -8,17 +8,29 @@ final class AlmanacCoreCustomField
return 'almanac:core';
}
public function createFields($object) {
$specs = array();
public function getFieldKey() {
return $this->getProxy()->getRawStandardFieldKey();
}
public function getFieldName() {
return $this->getFieldKey();
}
public function createFields($object) {
$specs = $object->getAlmanacPropertyFieldSpecifications();
$default_specs = array();
foreach ($object->getAlmanacProperties() as $property) {
$specs[$property->getFieldName()] = array(
$default_specs[$property->getFieldName()] = array(
'name' => $property->getFieldName(),
'type' => 'text',
);
}
return PhabricatorStandardCustomField::buildStandardFields($this, $specs);
return PhabricatorStandardCustomField::buildStandardFields(
$this,
$specs + $default_specs);
}
public function shouldUseStorage() {
@ -26,8 +38,11 @@ final class AlmanacCoreCustomField
}
public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
$key = $this->getProxy()->getRawStandardFieldKey();
$this->setValueFromStorage($object->getAlmanacPropertyValue($key));
$key = $this->getFieldKey();
if ($object->hasAlmanacProperty($key)) {
$this->setValueFromStorage($object->getAlmanacPropertyValue($key));
}
}
public function applyApplicationTransactionInternalEffects(
@ -40,7 +55,7 @@ final class AlmanacCoreCustomField
$object = $this->getObject();
$phid = $object->getPHID();
$key = $this->getProxy()->getRawStandardFieldKey();
$key = $this->getFieldKey();
$property = id(new AlmanacPropertyQuery())
->setViewer($this->getViewer())

View file

@ -7,5 +7,6 @@ interface AlmanacPropertyInterface {
public function hasAlmanacProperty($key);
public function getAlmanacProperty($key);
public function getAlmanacPropertyValue($key, $default = null);
public function getAlmanacPropertyFieldSpecifications();
}

View file

@ -14,7 +14,7 @@ final class AlmanacPropertyQuery
}
public function withObjectPHIDs(array $phids) {
$this->phids = $phids;
$this->objectPHIDs = $phids;
return $this;
}

View file

@ -12,7 +12,7 @@ abstract class AlmanacQuery
$property_query = id(new AlmanacPropertyQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withObjectPHIDs(mpull($objects, null, 'getPHID'));
->withObjectPHIDs(mpull($objects, 'getPHID'));
// NOTE: We disable policy filtering and object attachment to avoid
// a cyclic dependency where objects need their properties and properties
@ -21,6 +21,7 @@ abstract class AlmanacQuery
$property_query->setDisablePolicyFilteringAndAttachment(true);
$properties = $property_query->execute();
$properties = mgroup($properties, 'getObjectPHID');
foreach ($objects as $object) {
$object_properties = idx($properties, $object->getPHID(), array());

View file

@ -16,4 +16,19 @@ final class AlmanacClusterRepositoryServiceType
'Defines a repository service for use in a Phabricator cluster.');
}
public function getFieldSpecifications() {
return array(
'closed' => array(
'type' => 'bool',
'name' => pht('Closed'),
'default' => false,
'strings' => array(
'edit.checkbox' => pht(
'Prevent new repositories from being allocated on this '.
'service.'),
),
),
);
}
}

View file

@ -47,6 +47,14 @@ abstract class AlmanacServiceType extends Phobject {
}
public function getDefaultPropertyMap() {
return array();
}
public function getFieldSpecifications() {
return array();
}
/**
* List all available service type implementations.
*

View file

@ -119,6 +119,10 @@ final class AlmanacBinding
}
}
public function getAlmanacPropertyFieldSpecifications() {
return array();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -97,6 +97,10 @@ final class AlmanacDevice
}
}
public function getAlmanacPropertyFieldSpecifications() {
return array();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -40,7 +40,7 @@ final class AlmanacNetwork
}
public function getURI() {
return '/almanac/network/view/'.$this->getName().'/';
return '/almanac/network/'.$this->getID().'/';
}

View file

@ -121,6 +121,10 @@ final class AlmanacService
}
}
public function getAlmanacPropertyFieldSpecifications() {
return $this->getServiceType()->getFieldSpecifications();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -22,9 +22,13 @@ final class PhabricatorStandardCustomFieldBool
return $this->newNumericIndex(0);
}
public function readValueFromRequest(AphrontRequest $request) {
$this->setFieldValue((bool)$request->getBool($this->getFieldKey()));
}
public function getValueForStorage() {
$value = $this->getFieldValue();
if (strlen($value)) {
if ($value !== null) {
return (int)$value;
} else {
return null;

View file

@ -0,0 +1,7 @@
/**
* @provides almanac-css
*/
.almanac-default-property-value {
color: {$lightgreytext};
}