diff --git a/application/controllers/HostsController.php b/application/controllers/HostsController.php index 2d677d67..8fe23a09 100644 --- a/application/controllers/HostsController.php +++ b/application/controllers/HostsController.php @@ -6,4 +6,8 @@ use Icinga\Module\Director\Web\Controller\ObjectsController; class HostsController extends ObjectsController { + protected $multiEdit = array( + 'imports', + 'groups' + ); } diff --git a/application/forms/IcingaMultiEditForm.php b/application/forms/IcingaMultiEditForm.php new file mode 100644 index 00000000..e45e6ddb --- /dev/null +++ b/application/forms/IcingaMultiEditForm.php @@ -0,0 +1,270 @@ +objects = $objects; + $this->object = current($this->objects); + $this->db = $this->object()->getConnection(); + return $this; + } + + public function pickElementsFrom(QuickForm $form, $properties) + { + $this->relatedForm = $form; + $this->propertiesToPick = $properties; + return $this; + } + + public function setup() + { + $object = $this->object; + + $loader = new IcingaObjectFieldLoader($object); + $loader->addFieldsToForm($this); + + if ($form = $this->relatedForm) { + $form->setDb($object->getConnection()) + ->setObject($object) + ->prepareElements(); + } else { + $this->propertiesToPick = array(); + } + + foreach ($this->propertiesToPick as $property) { + if ($el = $form->getElement($property)) { + $this->makeVariants($el); + } + } + + foreach ($this->getElements() as $el) { + $name = $el->getName(); + if (substr($name, 0, 4) === 'var_') { + $this->makeVariants($el); + } + } + + $this->setButtons(); + } + + public function onSuccess() + { + foreach ($this->getValues() as $key => $value) { + $this->setSubmittedMultiValue($key, $value); + } + + $modified = $this->storeModifiedObjects(); + if ($modified === 0) { + $msg = $this->translate('No object has been modified'); + } elseif ($modified === 1) { + $msg = $this->translate('One object has been modified'); + } else { + $msg = sprintf( + $this->translate('%d objects have been modified'), + $modified + ); + } + + $this->redirectOnSuccess($msg); + } + + /** + * No default objects behaviour + */ + protected function onRequest() + { + } + + protected function setSubmittedMultiValue($key, $value) + { + $parts = preg_split('/_/', $key); + $objectsSum = array_pop($parts); + $valueSum = array_pop($parts); + $property = implode('_', $parts); + + $found = false; + foreach ($this->getVariants($property) as $json => $objects) { + if ($valueSum !== sha1($json)) { + continue; + } + + if ($objectsSum !== sha1(json_encode($objects))) { + continue; + } + + $found = true; + if (substr($property, 0, 4) === 'var_') { + $property = 'vars.' . substr($property, 4); + } + + foreach ($this->getObjects($objects) as $object) { + $object->$property = $value; + } + } + } + + protected function storeModifiedObjects() + { + $modified = 0; + foreach ($this->objects as $object) { + if ($object->hasBeenModified()) { + $modified++; + $object->store(); + } + } + + return $modified; + } + + protected function getDisplayGroupForElement(ZfElement $element) + { + if ($this->elementGroupMap === null) { + $this->resolveDisplayGroups(); + } + + $name = $element->getName(); + if (array_key_exists($name, $this->elementGroupMap)) { + $groupName = $this->elementGroupMap[$name]; + + if ($group = $this->getDisplayGroup($groupName)) { + return $group; + } elseif ($this->relatedForm) { + return $this->stealDisplayGroup($groupName, $this->relatedForm); + } + } else { + return null; + } + } + + protected function stealDisplayGroup($name, $form) + { + if ($group = $this->relatedForm->getDisplayGroup($name)) { + $group = clone($group); + $group->setElements(array()); + $this->_displayGroups[$name] = $group; + $this->_order[$name] = $this->_displayGroups[$name]->getOrder(); + $this->_orderUpdated = true; + + return $group; + } + + return null; + } + + protected function resolveDisplayGroups() + { + $this->elementGroupMap = array(); + if ($form = $this->relatedForm) { + $this->extractFormDisplayGroups($form, true); + } + + $this->extractFormDisplayGroups($this); + } + + protected function extractFormDisplayGroups($form, $clone = false) + { + foreach ($form->getDisplayGroups() as $group) { + $groupName = $group->getName(); + foreach ($group->getElements() as $name => $e) { + $this->elementGroupMap[$name] = $groupName; + } + } + } + + protected function makeVariants(ZfElement $element) + { + $key = $element->getName(); + $this->removeElement($key); + $label = $element->getLabel(); + $group = $this->getDisplayGroupForElement($element); + $description = $element->getDescription(); + + foreach ($this->getVariants($key) as $json => $objects) { + $value = json_decode($json); + $checksum = sha1($json) . '_' . sha1(json_encode($objects)); + + $v = clone($element); + $v->setName($key . '_' . $checksum); + $v->setDescription($description . '. ' . $this->descriptionForObjects($objects)); + $v->setLabel($label . $this->labelCount($objects)); + $v->setValue($value); + if ($group) { + $group->addElement($v); + } + $this->addElement($v); + } + } + + protected function getVariants($key) + { + $variants = array(); + if (substr($key, 0, 4) === 'var_') { + $key = 'vars.' . substr($key, 4); + } + + foreach ($this->objects as $name => $object) { + $value = json_encode($object->$key); + if (! array_key_exists($value, $variants)) { + $variants[$value] = array(); + } + + $variants[$value][] = $name; + } + + foreach ($variants as & $objects) { + natsort($objects); + } + + return $variants; + } + + protected function descriptionForObjects($list) + { + return sprintf( + $this->translate('Changing this value affects %d object(s): %s'), + count($list), + implode(', ', $list) + ); + } + + protected function labelCount($list) + { + return ' (' . count($list) . ')'; + } + + protected function db() + { + if ($this->db === null) { + $this->db = $this->object()->getConnection(); + } + + return $this->db; + } + + protected function getObjects($names) + { + $res = array(); + foreach ($names as $name) { + $res[$name] = $this->objects[$name]; + } + + return $res; + } +} diff --git a/application/tables/IcingaHostTable.php b/application/tables/IcingaHostTable.php index 799d4ddf..f6054358 100644 --- a/application/tables/IcingaHostTable.php +++ b/application/tables/IcingaHostTable.php @@ -34,6 +34,15 @@ class IcingaHostTable extends IcingaObjectTable return $this->url('director/host', array('name' => $row->host)); } + protected function getMultiselectProperties() + { + return array( + 'url' => 'director/hosts/edit', + 'sourceUrl' => 'director/hosts', + 'keys' => array('name'), + ); + } + public function getTitles() { $view = $this->view(); diff --git a/library/Director/Web/Controller/ActionController.php b/library/Director/Web/Controller/ActionController.php index aa6c859d..d1de32c6 100644 --- a/library/Director/Web/Controller/ActionController.php +++ b/library/Director/Web/Controller/ActionController.php @@ -90,6 +90,17 @@ abstract class ActionController extends Controller $this->sendJson((object) array('error' => $message)); } + protected function singleTab($label) + { + return $this->view->tabs = Widget::create('tabs')->add( + 'tab', + array( + 'label' => $label, + 'url' => $this->getRequest()->getUrl() + ) + )->activate('tab'); + } + protected function setConfigTabs() { $this->view->tabs = Widget::create('tabs')->add( diff --git a/library/Director/Web/Controller/ObjectsController.php b/library/Director/Web/Controller/ObjectsController.php index 1941c576..79fbac09 100644 --- a/library/Director/Web/Controller/ObjectsController.php +++ b/library/Director/Web/Controller/ObjectsController.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Director\Web\Controller; +use Icinga\Exception\NotFoundError; use Icinga\Data\Filter\Filter; use Icinga\Module\Director\Objects\IcingaObject; use Icinga\Module\Director\Web\Table\IcingaObjectTable; @@ -12,6 +13,8 @@ abstract class ObjectsController extends ActionController protected $isApified = true; + protected $multiEdit = array(); + protected $globalTypes = array( 'ApiUser', 'Zone', @@ -194,6 +197,41 @@ abstract class ObjectsController extends ActionController $this->setViewScript('objects/table'); } + public function editAction() + { + $type = ucfirst($this->getType()); + + if (empty($this->multiEdit)) { + throw new NotFoundError('Cannot edit multiple "%s" instances', $type); + } + $formName = 'icinga' . $type; + + $this->singleTab($this->translate('Multiple objects')); + $filter = Filter::fromQueryString($this->params->toString()); + $dummy = $this->dummyObject(); + $objects = array(); + $db = $this->db(); + foreach ($filter->filters() as $sub) { + foreach ($sub->filters() as $ex) { + if ($ex->isExpression() && $ex->getColumn() === 'name') { + $name = $ex->getExpression(); + $objects[$name] = $dummy::load($name, $db); + } + } + } + $this->view->title = sprintf( + $this->translate('Modify %d objects'), + count($objects) + ); + + $this->view->form = $this->loadForm('IcingaMultiEdit') + ->setObjects($objects) + ->pickElementsFrom($this->loadForm($formName), $this->multiEdit) + ->handleRequest(); + + $this->setViewScript('objects/form'); + } + public function templatesAction() { $this->indexAction(); diff --git a/library/Director/Web/Form/DirectorObjectForm.php b/library/Director/Web/Form/DirectorObjectForm.php index 69b505bc..08eb2452 100644 --- a/library/Director/Web/Form/DirectorObjectForm.php +++ b/library/Director/Web/Form/DirectorObjectForm.php @@ -257,7 +257,11 @@ abstract class DirectorObjectForm extends QuickForm protected function handleCustomVars($object, & $values) { if ($this->assertResolvedImports()) { - IcingaObjectFieldLoader::addFieldsToForm($this, $object, $values); + $loader = new IcingaObjectFieldLoader($object); + $loader->addFieldsToForm($this); + if ($values) { + $loader->setValues($values, 'var_'); + } } } @@ -453,7 +457,7 @@ abstract class DirectorObjectForm extends QuickForm { $this->object = $object; if ($this->db === null) { - $this->setDb($db); + $this->setDb($object->getConnection()); } return $this; diff --git a/library/Director/Web/Form/IcingaObjectFieldLoader.php b/library/Director/Web/Form/IcingaObjectFieldLoader.php index 5076dac5..640c2417 100644 --- a/library/Director/Web/Form/IcingaObjectFieldLoader.php +++ b/library/Director/Web/Form/IcingaObjectFieldLoader.php @@ -2,10 +2,11 @@ namespace Icinga\Module\Director\Web\Form; +use stdClass; use Icinga\Module\Director\Objects\IcingaObject; use Icinga\Module\Director\Objects\IcingaServiceSet; use Icinga\Module\Director\Objects\DirectorDatafield; -use stdClass; +use Zend_Form_Element as ZfElement; class IcingaObjectFieldLoader { @@ -13,95 +14,172 @@ class IcingaObjectFieldLoader protected $object; - protected function __construct(DirectorObjectForm $form, IcingaObject $object) + protected $fields; + + protected $elements; + + public function __construct(IcingaObject $object) { - $this->form = $form; $this->object = $object; } - public static function addFieldsToForm(DirectorObjectForm $form, IcingaObject $object, & $values) + public function addFieldsToForm(QuickForm $form) { - if (! $object->supportsCustomVars()) { - return $form; + if ($this->object->supportsCustomVars()) { + $this->attachFieldsToForm($form); } - $loader = new static($form, $object); - $loader->addFields(); - if ($values !== null) { - $loader->setValues($loader->stripKeyPrefix($values, 'var_')); - } - - return $form; + return $this; } - protected function stripKeyPrefix($array, $prefix) + /** + * Set a list of values + * + * Works in a failsafe way, when a field does not exist the value will be + * silently ignored + * + * @param Array $values key/value pairs with variable names and their value + * @param String $prefix An optional prefix that would be stripped from keys + * + * @return self + */ + public function setValues($values, $prefix = null) { - $new = array(); - $len = strlen($prefix); - foreach ($array as $key => $value) { - if (substr($key, 0, $len) === $prefix) { - $new[substr($key, $len)] = $value; - } + if ($prefix !== null) { + $len = strlen($prefix); } - - return $new; - } - - protected function setValues($values) - { $vars = $this->object->vars(); - $form = $this->form; foreach ($values as $key => $value) { - if ($el = $form->getElement('var_' . $key)) { - if ($value === '' || $value === null) { + if ($prefix) { + if (substr($key, 0, $len) === $prefix) { + $key = substr($key, $len); + } else { continue; } + } + + if ($el = $this->getElement($key)) { $el->setValue($value); - $vars->set($key, $el->getValue()); + $value = $el->getValue(); + + if ($value === '') { + $value = null; + } + + $vars->set($key, $value); + } + } + + return $this; + } + + /** + * Get the fields for our object + * + * @return DirectorDatafield[] + */ + public function getFields() + { + if ($this->fields === null) { + $this->fields = $this->prepareObjectFields($this->object); + } + + return $this->fields; + } + + /** + * Get the form elements for our fields + * + * @param QuickForm $form Optional + * + * @return ZfElement[] + */ + public function getElements(QuickForm $form = null) + { + if ($this->elements === null) { + $this->elements = $this->createElements($form); + $this->setValuesFromObject($this->object); + } + + return $this->elements; + } + + /** + * Attach our form fields to the given form + * + * This will also create a 'Custom properties' display group + */ + protected function attachFieldsToForm(QuickForm $form) + { + $elements = $this->getElements($form); + + if (! empty($elements)) { + $form->addElementsToGroup( + $elements, + 'custom_fields', + 50, + $form->translate('Custom properties') + ); + } + } + + /** + * Get the form element for a specific field by it's variable name + * + * @return ZfElement|null + */ + protected function getElement($name) + { + $elements = $this->getElements(); + if (array_key_exists($name, $elements)) { + return $this->elements[$name]; + } + + return null; + } + + /** + * Get the form elements based on the given form + * + * @return ZfElement[] + */ + protected function createElements(QuickForm $form) + { + $elements = array(); + + foreach ($this->getFields() as $name => $field) { + $elements[$name] = $field->getFormElement($form); + } + + return $elements; + } + + protected function setValuesFromObject(IcingaObject $object) + { + foreach ($object->getVars() as $k => $v) { + if ($v !== null && $el = $this->getElement($k)) { + $el->setValue($v); } } } - protected function addFields() + protected function mergeFields($listOfFields) { - $object = $this->object; - if ($object instanceof IcingaServiceSet) { - } else { - $this->attachFields( - $this->prepareObjectFields($object) - ); - } - - $this->setValues($object->getVars()); - } - - protected function attachFields($fields) - { - $form = $this->form; - $elements = array(); - foreach ($fields as $field) { - $elements[] = $field->getFormElement($form); - } - - if (empty($elements)) { - return $this; - } - - return $form->addElementsToGroup( - $elements, - 'custom_fields', - 50, - $form->translate('Custom properties') - ); + // TODO: Merge field for different object, mostly sets } + /** + * Create the fields for our object + * + * + * @return DirectorDatafield[] + */ protected function prepareObjectFields($object) { $fields = $this->loadResolvedFieldsForObject($object); - - if ($object->hasProperty('command_id')) { - $command = $object->getResolvedRelated('command'); + if ($object->hasRelation('check_command')) { + $command = $object->getResolvedRelated('check_command'); if ($command) { $cmdFields = $this->loadResolvedFieldsForObject($command); foreach ($cmdFields as $varname => $field) { @@ -115,11 +193,14 @@ class IcingaObjectFieldLoader return $fields; } - protected function mergeFields($listOfFields) - { - // TODO: Merge field for different object, mostly sets - } - + /** + * Create the fields for our object + * + * Follows the inheritance logic, resolves all fields and keeps the most + * specific ones. Returns a list of fields indexed by variable name + * + * @return DirectorDatafield[] + */ protected function loadResolvedFieldsForObject($object) { $result = $this->loadDataFieldsForObjects( @@ -139,11 +220,16 @@ class IcingaObjectFieldLoader return $fields; } - protected function getDb() - { - return $this->form->getDb(); - } - + /** + * Fetches fields for a given List of objects from the database + * + * Gives a list indexed by object id, with each entry being a list of that + * objects DirectorDatafield instances indexed by variable name + * + * @param IcingaObject[] $objectList List of objects + * + * @return Array + */ protected function loadDataFieldsForObjects($objectList) { $ids = array(); @@ -163,7 +249,7 @@ class IcingaObjectFieldLoader $db = $connection->getDbAdapter(); $idColumn = 'f.' . $object->getShortTableName() . '_id'; - + $query = $db->select()->from( array('df' => 'director_datafield'), array( @@ -193,7 +279,7 @@ class IcingaObjectFieldLoader if (! array_key_exists($id, $result)) { $result[$id] = new stdClass; } - + $result[$id]->{$r->varname} = DirectorDatafield::fromDbRow( $r, $connection diff --git a/library/Director/Web/Table/QuickTable.php b/library/Director/Web/Table/QuickTable.php index 3c707111..94fd2de5 100644 --- a/library/Director/Web/Table/QuickTable.php +++ b/library/Director/Web/Table/QuickTable.php @@ -67,6 +67,41 @@ abstract class QuickTable implements Paginatable } } + protected function getMultiselectProperties() + { + /* array( + * 'url' => 'director/hosts/edit', + * 'sourceUrl' => 'director/hosts', + * 'keys' => 'name' + * ) */ + + return array(); + } + + protected function renderMultiselectAttributes() + { + $props = $this->getMultiselectProperties(); + + if (empty($props)) { + return ''; + } + + $prefix = 'data-icinga-multiselect-'; + $view = $this->view(); + $parts = array(); + $multi = array( + 'url' => $view->href($props['url']), + 'controllers' => $view->href($props['sourceUrl']), + 'data' => implode(',', $props['keys']), + ); + + foreach ($multi as $k => $v) { + $parts[] = $prefix . $k . '="' . $v . '"'; + } + + return ' ' . implode(' ', $parts); + } + protected function renderRow($row) { $htm = " getRowClassesString($row) . ">\n"; @@ -245,14 +280,22 @@ abstract class QuickTable implements Paginatable protected function listTableClasses() { - return array('simple', 'common-table', 'table-row-selectable'); + $classes = array('simple', 'common-table', 'table-row-selectable'); + if (! empty($this->getMultiselectProperties())) { + $classes[] = 'multiselect'; + } + + return $classes; } public function render() { $data = $this->fetchData(); - $htm = 'createClassAttribute($this->listTableClasses()) . '>' . "\n" + $htm = 'createClassAttribute($this->listTableClasses()) + . $this->renderMultiselectAttributes() + . '>' . "\n" . $this->renderTitles($this->getTitles()) . "\n"; foreach ($data as $row) { diff --git a/public/js/module.js b/public/js/module.js index 76b1543d..aaf3c767 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -99,15 +99,16 @@ extensibleSetAction: function(ev) { var el = ev.currentTarget; - if (el.name.match(/__MOVE_UP$/)) { var $li = $(el).closest('li'); - var $prev = $li.prev() + var $prev = $li.prev(); + // TODO: document what's going on here. if ($li.find('input[type=text].autosubmit')) { if (iid = $prev.find('input[type=text]').attr('id')) { $li.closest('.container').data('activeExtensibleEntry', iid); + } else { + return true; } - return true; } if ($prev.length) { $prev.before($li.detach()); @@ -118,12 +119,14 @@ return false; } else if (el.name.match(/__MOVE_DOWN$/)) { var $li = $(el).closest('li'); - var $next = $li.next() + var $next = $li.next(); + // TODO: document what's going on here. if ($li.find('input[type=text].autosubmit')) { if (iid = $next.find('input[type=text]').attr('id')) { $li.closest('.container').data('activeExtensibleEntry', iid); + } else { + return true; } - return true; } if ($next.length && ! $next.find('.extend-set').length) { $next.after($li.detach());