diff --git a/src/applications/almanac/controller/AlmanacController.php b/src/applications/almanac/controller/AlmanacController.php index d6d7188511..24a8986766 100644 --- a/src/applications/almanac/controller/AlmanacController.php +++ b/src/applications/almanac/controller/AlmanacController.php @@ -1,214 +1,215 @@ getViewer(); $properties = $object->getAlmanacProperties(); $this->requireResource('almanac-css'); Javelin::initBehavior('phabricator-tooltips', array()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); $properties = $object->getAlmanacProperties(); $icon_builtin = id(new PHUIIconView()) ->setIcon('fa-circle') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Builtin Property'), 'align' => 'E', )); $icon_custom = id(new PHUIIconView()) ->setIcon('fa-circle-o grey') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Custom Property'), 'align' => 'E', )); $builtins = $object->getAlmanacPropertyFieldSpecifications(); - $defaults = mpull($builtins, null, 'getValueForTransaction'); + $defaults = mpull($builtins, 'getValueForTransaction'); // Sort fields so builtin fields appear first, then fields are ordered // alphabetically. $properties = msort($properties, 'getFieldName'); $head = array(); $tail = array(); foreach ($properties as $property) { $key = $property->getFieldName(); if (isset($builtins[$key])) { $head[$key] = $property; } else { $tail[$key] = $property; } } $properties = $head + $tail; $delete_base = $this->getApplicationURI('property/delete/'); $edit_base = $this->getApplicationURI('property/update/'); $rows = array(); foreach ($properties as $key => $property) { $value = $property->getFieldValue(); $is_builtin = isset($builtins[$key]); + $is_persistent = (bool)$property->getID(); $delete_uri = id(new PhutilURI($delete_base)) ->setQueryParams( array( 'key' => $key, 'objectPHID' => $object->getPHID(), )); $edit_uri = id(new PhutilURI($edit_base)) ->setQueryParams( array( 'key' => $key, 'objectPHID' => $object->getPHID(), )); $delete = javelin_tag( 'a', array( - 'class' => ($can_edit + 'class' => (($can_edit && $is_persistent) ? 'button button-grey small' : 'button 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( ($is_builtin ? $icon_builtin : $icon_custom), $display_key, $display_value, $delete, ); } $table = id(new AphrontTableView($rows)) ->setNoDataString(pht('No properties.')) ->setHeaders( array( null, pht('Name'), pht('Value'), null, )) ->setColumnClasses( array( null, null, 'wide', 'action', )); $phid = $object->getPHID(); $add_uri = id(new PhutilURI($edit_base)) ->setQueryParam('objectPHID', $object->getPHID()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); $add_button = id(new PHUIButtonView()) ->setTag('a') ->setHref($add_uri) ->setWorkflow(true) ->setDisabled(!$can_edit) ->setText(pht('Add Property')) ->setIcon('fa-plus'); $header = id(new PHUIHeaderView()) ->setHeader(pht('Properties')) ->addActionLink($add_button); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($table); } protected function addClusterMessage( $positive, $negative) { $can_manage = $this->hasApplicationCapability( AlmanacManageClusterServicesCapability::CAPABILITY); $doc_link = phutil_tag( 'a', array( 'href' => PhabricatorEnv::getDoclink( 'Clustering Introduction'), 'target' => '_blank', ), pht('Learn More')); if ($can_manage) { $severity = PHUIInfoView::SEVERITY_NOTICE; $message = $positive; } else { $severity = PHUIInfoView::SEVERITY_WARNING; $message = $negative; } $icon = id(new PHUIIconView()) ->setIcon('fa-sitemap'); return id(new PHUIInfoView()) ->setSeverity($severity) ->setErrors( array( array($icon, ' ', $message, ' ', $doc_link), )); } protected function getPropertyDeleteURI($object) { return null; } protected function getPropertyUpdateURI($object) { return null; } } diff --git a/src/applications/almanac/editor/AlmanacPropertyEditEngine.php b/src/applications/almanac/editor/AlmanacPropertyEditEngine.php index e5bfd2822a..38139f79fa 100644 --- a/src/applications/almanac/editor/AlmanacPropertyEditEngine.php +++ b/src/applications/almanac/editor/AlmanacPropertyEditEngine.php @@ -1,79 +1,86 @@ propertyKey = $property_key; return $this; } public function getPropertyKey() { return $this->propertyKey; } public function isEngineConfigurable() { return false; } public function isEngineExtensible() { return false; } public function getEngineName() { return pht('Almanac Properties'); } public function getSummaryHeader() { return pht('Edit Almanac Property Configurations'); } public function getSummaryText() { return pht('This engine is used to edit Almanac properties.'); } public function getEngineApplicationClass() { return 'PhabricatorAlmanacApplication'; } protected function newEditableObject() { throw new PhutilMethodNotImplementedException(); } protected function getObjectCreateTitleText($object) { return pht('Create Property'); } protected function getObjectCreateButtonText($object) { return pht('Create Property'); } protected function getObjectEditTitleText($object) { return pht('Edit Property: %s', $object->getName()); } protected function getObjectEditShortText($object) { return pht('Edit Property'); } protected function getObjectCreateShortText() { return pht('Create Property'); } protected function buildCustomEditFields($object) { $property_key = $this->getPropertyKey(); $xaction_type = $object->getAlmanacPropertySetTransactionType(); + $specs = $object->getAlmanacPropertyFieldSpecifications(); + if (isset($specs[$property_key])) { + $field_template = clone $specs[$property_key]; + } else { + $field_template = new PhabricatorTextEditField(); + } + return array( - id(new PhabricatorTextEditField()) + $field_template ->setKey('value') ->setMetadataValue('almanac.property', $property_key) ->setLabel($property_key) ->setTransactionType($xaction_type) ->setValue($object->getAlmanacPropertyValue($property_key)), ); } } diff --git a/src/applications/almanac/query/AlmanacQuery.php b/src/applications/almanac/query/AlmanacQuery.php index b046171d32..b5b6146c7f 100644 --- a/src/applications/almanac/query/AlmanacQuery.php +++ b/src/applications/almanac/query/AlmanacQuery.php @@ -1,59 +1,61 @@ needProperties = $need_properties; return $this; } protected function getNeedProperties() { return $this->needProperties; } protected function didFilterPage(array $objects) { $has_properties = (head($objects) instanceof AlmanacPropertyInterface); if ($has_properties && $this->needProperties) { $property_query = id(new AlmanacPropertyQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withObjects($objects); $properties = $property_query->execute(); $properties = mgroup($properties, 'getObjectPHID'); foreach ($objects as $object) { $object_properties = idx($properties, $object->getPHID(), array()); $object_properties = mpull($object_properties, null, 'getFieldName'); // Create synthetic properties for defaults on the object itself. $specs = $object->getAlmanacPropertyFieldSpecifications(); foreach ($specs as $key => $spec) { if (empty($object_properties[$key])) { + $default_value = $spec->getValueForTransaction(); + $object_properties[$key] = id(new AlmanacProperty()) ->setObjectPHID($object->getPHID()) ->setFieldName($key) - ->setFieldValue($spec->getValueForTransaction()); + ->setFieldValue($default_value); } } foreach ($object_properties as $property) { $property->attachObject($object); } $object->attachAlmanacProperties($object_properties); } } return $objects; } public function getQueryApplicationClass() { return 'PhabricatorAlmanacApplication'; } } diff --git a/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php b/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php index 259092b5b1..fffe94997a 100644 --- a/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php @@ -1,27 +1,59 @@ id(new PhabricatorTextEditField()), ); } + public function getBindingFieldSpecifications(AlmanacBinding $binding) { + $protocols = array( + array( + 'value' => 'http', + 'port' => 80, + ), + array( + 'value' => 'https', + 'port' => 443, + ), + array( + 'value' => 'ssh', + 'port' => 22, + ), + ); + + $default_value = 'http'; + if ($binding->hasInterface()) { + $interface = $binding->getInterface(); + $port = $interface->getPort(); + + $default_ports = ipull($protocols, 'value', 'port'); + $default_value = idx($default_ports, $port, $default_value); + } + + return array( + 'protocol' => id(new PhabricatorSelectEditField()) + ->setOptions(ipull($protocols, 'value', 'value')) + ->setValue($default_value), + ); + } + } diff --git a/src/applications/almanac/servicetype/AlmanacServiceType.php b/src/applications/almanac/servicetype/AlmanacServiceType.php index 2eb56dc8c4..2f7f503fe8 100644 --- a/src/applications/almanac/servicetype/AlmanacServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacServiceType.php @@ -1,77 +1,81 @@ getPhobjectClassConstant('SERVICETYPE', 64); } public function getServiceTypeIcon() { return 'fa-cog'; } /** * Return `true` if this service type is a Phabricator cluster service type. * * These special services change the behavior of Phabricator, and require * elevated permission to create and edit. * * @return bool True if this is a Phabricator cluster service type. */ public function isClusterServiceType() { return false; } public function getDefaultPropertyMap() { return array(); } public function getFieldSpecifications() { return array(); } + public function getBindingFieldSpecifications(AlmanacBinding $binding) { + return array(); + } + /** * List all available service type implementations. * * @return map Dictionary of available service types. */ public static function getAllServiceTypes() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getServiceTypeConstant') ->setSortMethod('getServiceTypeName') ->execute(); } } diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php index be70a5ecdc..c593e40fa7 100644 --- a/src/applications/almanac/storage/AlmanacBinding.php +++ b/src/applications/almanac/storage/AlmanacBinding.php @@ -1,270 +1,274 @@ setServicePHID($service->getPHID()) ->attachService($service) ->attachAlmanacProperties(array()) ->setIsDisabled(0); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'mailKey' => 'bytes20', 'isDisabled' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_service' => array( 'columns' => array('servicePHID', 'interfacePHID'), 'unique' => true, ), 'key_device' => array( 'columns' => array('devicePHID'), ), 'key_interface' => array( 'columns' => array('interfacePHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(AlmanacBindingPHIDType::TYPECONST); } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } return parent::save(); } public function getName() { return pht('Binding %s', $this->getID()); } public function getURI() { return '/almanac/binding/'.$this->getID().'/'; } public function getService() { return $this->assertAttached($this->service); } public function attachService(AlmanacService $service) { $this->service = $service; return $this; } public function getDevice() { return $this->assertAttached($this->device); } public function attachDevice(AlmanacDevice $device) { $this->device = $device; return $this; } + public function hasInterface() { + return ($this->interface !== self::ATTACHABLE); + } + public function getInterface() { return $this->assertAttached($this->interface); } public function attachInterface(AlmanacInterface $interface) { $this->interface = $interface; return $this; } /* -( AlmanacPropertyInterface )------------------------------------------- */ public function attachAlmanacProperties(array $properties) { assert_instances_of($properties, 'AlmanacProperty'); $this->almanacProperties = mpull($properties, null, 'getFieldName'); return $this; } public function getAlmanacProperties() { return $this->assertAttached($this->almanacProperties); } public function hasAlmanacProperty($key) { $this->assertAttached($this->almanacProperties); return isset($this->almanacProperties[$key]); } public function getAlmanacProperty($key) { return $this->assertAttachedKey($this->almanacProperties, $key); } public function getAlmanacPropertyValue($key, $default = null) { if ($this->hasAlmanacProperty($key)) { return $this->getAlmanacProperty($key)->getFieldValue(); } else { return $default; } } public function getAlmanacPropertyFieldSpecifications() { - return array(); + return $this->getService()->getBindingFieldSpecifications($this); } public function newAlmanacPropertyEditEngine() { return new AlmanacBindingPropertyEditEngine(); } public function getAlmanacPropertySetTransactionType() { return AlmanacBindingSetPropertyTransaction::TRANSACTIONTYPE; } public function getAlmanacPropertyDeleteTransactionType() { return AlmanacBindingDeletePropertyTransaction::TRANSACTIONTYPE; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getService()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getService()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { $notes = array( pht('A binding inherits the policies of its service.'), pht( 'To view a binding, you must also be able to view its device and '. 'interface.'), ); return $notes; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: if ($this->getService()->isClusterService()) { return array( array( new PhabricatorAlmanacApplication(), AlmanacManageClusterServicesCapability::CAPABILITY, ), ); } break; } return array(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new AlmanacBindingEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new AlmanacBindingTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->delete(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('servicePHID') ->setType('phid') ->setDescription(pht('The bound service.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('devicePHID') ->setType('phid') ->setDescription(pht('The device the service is bound to.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('interfacePHID') ->setType('phid') ->setDescription(pht('The interface the service is bound to.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('disabled') ->setType('bool') ->setDescription(pht('Interface status.')), ); } public function getFieldValuesForConduit() { return array( 'servicePHID' => $this->getServicePHID(), 'devicePHID' => $this->getDevicePHID(), 'interfacePHID' => $this->getInterfacePHID(), 'disabled' => (bool)$this->getIsDisabled(), ); } public function getConduitSearchAttachments() { return array( id(new AlmanacPropertiesSearchEngineAttachment()) ->setAttachmentKey('properties'), ); } } diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index 361eabca2a..ee40d83400 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -1,301 +1,306 @@ setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) ->attachAlmanacProperties(array()) ->setServiceType($type) ->attachServiceImplementation($implementation); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'nameIndex' => 'bytes12', 'mailKey' => 'bytes20', 'serviceType' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( 'columns' => array('nameIndex'), 'unique' => true, ), 'key_nametext' => array( 'columns' => array('name'), ), 'key_servicetype' => array( 'columns' => array('serviceType'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(AlmanacServicePHIDType::TYPECONST); } public function save() { AlmanacNames::validateName($this->getName()); $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } return parent::save(); } public function getURI() { return '/almanac/service/view/'.$this->getName().'/'; } public function getBindings() { return $this->assertAttached($this->bindings); } public function getActiveBindings() { $bindings = $this->getBindings(); // Filter out disabled bindings. foreach ($bindings as $key => $binding) { if ($binding->getIsDisabled()) { unset($bindings[$key]); } } return $bindings; } public function attachBindings(array $bindings) { $this->bindings = $bindings; return $this; } public function getServiceImplementation() { return $this->assertAttached($this->serviceImplementation); } public function attachServiceImplementation(AlmanacServiceType $type) { $this->serviceImplementation = $type; return $this; } public function isClusterService() { return $this->getServiceImplementation()->isClusterServiceType(); } /* -( AlmanacPropertyInterface )------------------------------------------- */ public function attachAlmanacProperties(array $properties) { assert_instances_of($properties, 'AlmanacProperty'); $this->almanacProperties = mpull($properties, null, 'getFieldName'); return $this; } public function getAlmanacProperties() { return $this->assertAttached($this->almanacProperties); } public function hasAlmanacProperty($key) { $this->assertAttached($this->almanacProperties); return isset($this->almanacProperties[$key]); } public function getAlmanacProperty($key) { return $this->assertAttachedKey($this->almanacProperties, $key); } public function getAlmanacPropertyValue($key, $default = null) { if ($this->hasAlmanacProperty($key)) { return $this->getAlmanacProperty($key)->getFieldValue(); } else { return $default; } } public function getAlmanacPropertyFieldSpecifications() { return $this->getServiceImplementation()->getFieldSpecifications(); } + public function getBindingFieldSpecifications(AlmanacBinding $binding) { + $impl = $this->getServiceImplementation(); + return $impl->getBindingFieldSpecifications($binding); + } + public function newAlmanacPropertyEditEngine() { return new AlmanacServicePropertyEditEngine(); } public function getAlmanacPropertySetTransactionType() { return AlmanacServiceSetPropertyTransaction::TRANSACTIONTYPE; } public function getAlmanacPropertyDeleteTransactionType() { return AlmanacServiceDeletePropertyTransaction::TRANSACTIONTYPE; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: if ($this->isClusterService()) { return array( array( new PhabricatorAlmanacApplication(), AlmanacManageClusterServicesCapability::CAPABILITY, ), ); } break; } return array(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new AlmanacServiceEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new AlmanacServiceTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $bindings = id(new AlmanacBindingQuery()) ->setViewer($engine->getViewer()) ->withServicePHIDs(array($this->getPHID())) ->execute(); foreach ($bindings as $binding) { $engine->destroyObject($binding); } $this->delete(); } /* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { return array( id(new AlmanacServiceNameNgrams()) ->setValue($this->getName()), ); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the service.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('serviceType') ->setType('string') ->setDescription(pht('The service type constant.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'serviceType' => $this->getServiceType(), ); } public function getConduitSearchAttachments() { return array( id(new AlmanacPropertiesSearchEngineAttachment()) ->setAttachmentKey('properties'), id(new AlmanacBindingsSearchEngineAttachment()) ->setAttachmentKey('bindings'), ); } }