1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-23 07:12: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/transaction.css' => '5d0cae25',
'rsrc/css/aphront/two-column.css' => '16ab3ad2', 'rsrc/css/aphront/two-column.css' => '16ab3ad2',
'rsrc/css/aphront/typeahead.css' => 'a989b5b3', 'rsrc/css/aphront/typeahead.css' => 'a989b5b3',
'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af',
'rsrc/css/application/auth/auth.css' => '1e655982', 'rsrc/css/application/auth/auth.css' => '1e655982',
'rsrc/css/application/base/main-menu-view.css' => '33e5f2f6', 'rsrc/css/application/base/main-menu-view.css' => '33e5f2f6',
'rsrc/css/application/base/notification-menu.css' => '6aa0a74b', 'rsrc/css/application/base/notification-menu.css' => '6aa0a74b',
@ -497,6 +498,7 @@ return array(
'rsrc/swf/aphlict.swf' => 'f19daffb', 'rsrc/swf/aphlict.swf' => 'f19daffb',
), ),
'symbols' => array( 'symbols' => array(
'almanac-css' => 'dbb9b3af',
'aphront-bars' => '231ac33c', 'aphront-bars' => '231ac33c',
'aphront-contextbar-view-css' => '1c3b0529', 'aphront-contextbar-view-css' => '1c3b0529',
'aphront-dark-console-css' => '6378ef3d', 'aphront-dark-console-css' => '6378ef3d',

View file

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

View file

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

View file

@ -9,13 +9,129 @@ abstract class AlmanacController
$viewer = $this->getViewer(); $viewer = $this->getViewer();
$properties = $object->getAlmanacProperties(); $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(); $rows = array();
foreach ($properties as $property) { foreach ($fields as $key => $field) {
$value = $property->getFieldValue(); $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( $rows[] = array(
$property->getFieldName(), ($is_builtin ? $icon_builtin : $icon_custom),
PhabricatorConfigJSON::prettyPrintJSON($value), $display_key,
$display_value,
$delete,
); );
} }
@ -23,13 +139,17 @@ abstract class AlmanacController
->setNoDataString(pht('No properties.')) ->setNoDataString(pht('No properties.'))
->setHeaders( ->setHeaders(
array( array(
null,
pht('Name'), pht('Name'),
pht('Value'), pht('Value'),
null,
)) ))
->setColumnClasses( ->setColumnClasses(
array( array(
null,
null, null,
'wide', 'wide',
'action',
)); ));
$phid = $object->getPHID(); $phid = $object->getPHID();

View file

@ -11,7 +11,6 @@ final class AlmanacNetworkViewController
$viewer = $request->getViewer(); $viewer = $request->getViewer();
$id = $request->getURIData('id'); $id = $request->getURIData('id');
$network = id(new AlmanacNetworkQuery()) $network = id(new AlmanacNetworkQuery())
->setViewer($viewer) ->setViewer($viewer)
->withIDs(array($id)) ->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) { public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer(); $viewer = $request->getViewer();
$id = $request->getURIData('id'); $object = id(new PhabricatorObjectQuery())
if ($id) { ->setViewer($viewer)
$property = id(new AlmanacPropertyQuery()) ->withPHIDs(array($request->getStr('objectPHID')))
->setViewer($viewer) ->requireCapabilities(
->withIDs(array($id)) array(
->requireCapabilities( PhabricatorPolicyCapability::CAN_VIEW,
array( PhabricatorPolicyCapability::CAN_EDIT,
PhabricatorPolicyCapability::CAN_VIEW, ))
PhabricatorPolicyCapability::CAN_EDIT, ->executeOne();
)) if (!$object) {
->executeOne(); return new Aphront404Response();
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');
} }
if (!($object instanceof AlmanacPropertyInterface)) { if (!($object instanceof AlmanacPropertyInterface)) {
@ -51,6 +25,21 @@ final class AlmanacPropertyEditController
$cancel_uri = $object->getURI(); $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) { if ($is_new) {
$errors = array(); $errors = array();
$property = null; $property = null;
@ -77,25 +66,11 @@ final class AlmanacPropertyEditController
} }
if (!$errors) { if (!$errors) {
$property = id(new AlmanacPropertyQuery()) $property_key = $name;
->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);
}
} }
} }
if (!$property) { if ($property_key === null) {
$form = id(new AphrontFormView()) $form = id(new AphrontFormView())
->setUser($viewer) ->setUser($viewer)
->appendChild( ->appendChild(
@ -115,17 +90,29 @@ final class AlmanacPropertyEditController
} }
} }
$v_name = $property->getFieldName(); // Make sure property key is appropriate.
$e_name = true; // TODO: It would be cleaner to put this safety check in the Editor.
AlmanacNames::validateServiceOrDeviceName($property_key);
$v_value = $property->getFieldValue(); // If we're adding a new property, put a placeholder on the object so
$e_value = null; // 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( $field_list = PhabricatorCustomField::getObjectFields(
$object, $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 $field_list
->setViewer($viewer) ->setViewer($viewer)
->readFieldsFromStorage($object); ->readFieldsFromStorage($object);
@ -153,7 +140,8 @@ final class AlmanacPropertyEditController
$form = id(new AphrontFormView()) $form = id(new AphrontFormView())
->setUser($viewer) ->setUser($viewer)
->addHiddenInput('objectPHID', $request->getStr('objectPHID')) ->addHiddenInput('objectPHID', $request->getStr('objectPHID'))
->addHiddenInput('name', $request->getStr('name')) ->addHiddenInput('key', $request->getStr('key'))
->addHiddenInput('name', $property_key)
->addHiddenInput('isValueEdit', true); ->addHiddenInput('isValueEdit', true);
$field_list->appendFieldsToForm($form); $field_list->appendFieldsToForm($form);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,7 +40,7 @@ final class AlmanacNetwork
} }
public function getURI() { 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 )----------------------------------------- */ /* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

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

View file

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