From 40e1b5a798b8d2a2853494c220ece009af2b32ca Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Fri, 15 Jan 2021 11:45:35 +0100 Subject: [PATCH] DataTypeDictionary: new data type fixes #337 --- application/controllers/DataController.php | 222 ++++++++++++++++++ .../IcingaServiceDictionaryMemberForm.php | 54 +++++ .../Director/DataType/DataTypeDictionary.php | 107 +++++++++ .../Web/Form/Element/InstanceSummary.php | 51 ++++ register-hooks.php | 2 + 5 files changed, 436 insertions(+) create mode 100644 application/forms/IcingaServiceDictionaryMemberForm.php create mode 100644 library/Director/DataType/DataTypeDictionary.php create mode 100644 library/Director/Web/Form/Element/InstanceSummary.php diff --git a/application/controllers/DataController.php b/application/controllers/DataController.php index 7ed5e123..7ef1ed10 100644 --- a/application/controllers/DataController.php +++ b/application/controllers/DataController.php @@ -2,10 +2,17 @@ namespace Icinga\Module\Director\Controllers; +use gipfl\Web\Widget\Hint; +use Icinga\Exception\NotFoundError; use Icinga\Module\Director\Forms\DirectorDatalistEntryForm; use Icinga\Module\Director\Forms\DirectorDatalistForm; +use Icinga\Module\Director\Forms\IcingaServiceDictionaryMemberForm; use Icinga\Module\Director\Objects\DirectorDatalist; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\PlainObjectRenderer; use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Form\IcingaObjectFieldLoader; use Icinga\Module\Director\Web\Table\CustomvarTable; use Icinga\Module\Director\Web\Table\DatafieldCategoryTable; use Icinga\Module\Director\Web\Table\DatafieldTable; @@ -13,6 +20,9 @@ use Icinga\Module\Director\Web\Table\DatalistEntryTable; use Icinga\Module\Director\Web\Table\DatalistTable; use Icinga\Module\Director\Web\Tabs\DataTabs; use gipfl\IcingaWeb2\Link; +use InvalidArgumentException; +use ipl\Html\Html; +use ipl\Html\Table; class DataController extends ActionController { @@ -140,6 +150,218 @@ class DataController extends ActionController $this->content()->add([$form, $table]); } + public function dictionaryAction() + { + $connection = $this->db(); + $this->addSingleTab('Nested Dictionary'); + $varName = $this->params->get('varname'); + $instance = $this->url()->getParam('instance'); + $action = $this->url()->getParam('action'); + $object = $this->requireObject(); + + if ($instance || $action) { + $this->actions()->add( + Link::create($this->translate('Back'), $this->url()->without(['action', 'instance']), null, [ + 'class' => 'icon-edit' + ]) + ); + } else { + $this->actions()->add( + Link::create($this->translate('Add'), $this->url(), [ + 'action' => 'add' + ], [ + 'class' => 'icon-edit' + ]) + ); + } + $subjects = $this->prepareSubjectsLabel($object, $varName); + $fieldLoader = new IcingaObjectFieldLoader($object); + $instances = $this->getCurrentInstances($object, $varName); + + if (empty($instances)) { + $this->content()->add(Hint::info(sprintf( + $this->translate('No %s have been created yet'), + $subjects + ))); + } else { + $this->content()->add($this->prepareInstancesTable($instances)); + } + + $field = $this->getFieldByName($fieldLoader, $varName); + $template = $object::load([ + 'object_name' => $field->getSetting('template_name') + ], $connection); + + $form = new IcingaServiceDictionaryMemberForm(); + $form->setDb($connection); + if ($instance) { + $instanceObject = $object::create([ + 'imports' => [$template], + 'object_name' => $instance, + 'vars' => $instances[$instance] + ], $connection); + $form->setObject($instanceObject); + } elseif ($action === 'add') { + $form->presetImports([$template->getObjectName()]); + } else { + return; + } + if ($instance) { + if (! isset($instances[$instance])) { + throw new NotFoundError("There is no such instance: $instance"); + } + $subTitle = sprintf($this->translate('Modify instance: %s'), $instance); + } else { + $subTitle = $this->translate('Add a new instance'); + } + + $this->content()->add(Html::tag('h2', ['style' => 'margin-top: 2em'], $subTitle)); + $form->handleRequest($this->getRequest()); + $this->content()->add($form); + if ($form->succeeded()) { + $virtualObject = $form->getObject(); + $name = $virtualObject->getObjectName(); + $params = $form->getObject()->getVars(); + $instances[$name] = $params; + if ($name !== $instance) { // Has been renamed + unset($instances[$instance]); + } + ksort($instances); + $object->set("vars.$varName", (object)$instances); + $object->store(); + $this->redirectNow($this->url()->without(['instance', 'action'])); + } elseif ($form->shouldBeDeleted()) { + unset($instances[$instance]); + if (empty($instances)) { + $object->set("vars.$varName", null)->store(); + } else { + $object->set("vars.$varName", (object)$instances)->store(); + } + $this->redirectNow($this->url()->without(['instance', 'action'])); + } + } + + protected function requireObject() + { + $connection = $this->db(); + $hostName = $this->params->getRequired('host'); + $serviceName = $this->params->get('service'); + if ($serviceName) { + $host = IcingaHost::load($hostName, $connection); + $object = IcingaService::load([ + 'host_id' => $host->get('id'), + 'object_name' => $serviceName, + ], $connection); + } else { + $object = IcingaHost::load($hostName, $connection); + } + + if (! $object->isObject()) { + throw new InvalidArgumentException(sprintf( + 'Only single objects allowed, %s is a %s', + $object->getObjectName(), + $object->get('object_type') + )); + } + return $object; + } + + protected function shorten($string, $maxLen) + { + if (strlen($string) <= $maxLen) { + return $string; + } + + return substr($string, 0, $maxLen) . '...'; + } + + protected function getFieldByName(IcingaObjectFieldLoader $loader, $name) + { + foreach ($loader->getFields() as $field) { + if ($field->get('varname') === $name) { + return $field; + } + } + + throw new InvalidArgumentException("Found no configured field for '$name'"); + } + + /** + * @param IcingaService $object + * @param $varName + * @return array + */ + protected function getCurrentInstances(IcingaService $object, $varName) + { + $currentVars = $object->getVars(); + if (isset($currentVars->$varName)) { + $currentValue = $currentVars->$varName; + } else { + $currentValue = (object)[]; + } + if (is_object($currentValue)) { + $currentValue = (array)$currentValue; + } else { + throw new InvalidArgumentException(sprintf( + '"%s" is not a valid Dictionary', + json_encode($currentValue) + )); + } + return $currentValue; + } + + /** + * @param array $currentValue + * @param $subjects + * @return Hint|Table + */ + protected function prepareInstancesTable(array $currentValue) + { + $table = new Table(); + $table->addAttributes([ + 'class' => 'common-table table-row-selectable' + ]); + $table->getHeader()->add( + Table::row([ + $this->translate('Key / Instance'), + $this->translate('Properties') + ], ['style' => 'text-align: left'], 'th') + ); + foreach ($currentValue as $key => $item) { + $table->add(Table::row([ + Link::create($key, $this->url()->with('instance', $key)), + str_replace("\n", ' ', $this->shorten(PlainObjectRenderer::render($item), 512)) + ])); + } + + return $table; + } + + /** + * @param IcingaService $object + * @param $varName + * @return string + */ + protected function prepareSubjectsLabel(IcingaService $object, $varName) + { + if ($object instanceof IcingaService) { + $hostName = $object->get('host'); + $subjects = $object->getObjectName() . " ($varName)"; + } else { + $hostName = $object->getObjectName(); + $subjects = sprintf( + $this->translate('%s instances'), + $varName + ); + } + $this->addTitle(sprintf( + $this->translate('%s on %s'), + $subjects, + $hostName + )); + return $subjects; + } + protected function addListActions(DirectorDatalist $list) { $this->actions()->add( diff --git a/application/forms/IcingaServiceDictionaryMemberForm.php b/application/forms/IcingaServiceDictionaryMemberForm.php new file mode 100644 index 00000000..90b8f94b --- /dev/null +++ b/application/forms/IcingaServiceDictionaryMemberForm.php @@ -0,0 +1,54 @@ +addHidden('object_type', 'object'); + $this->addElement('text', 'object_name', [ + 'label' => $this->translate('Name'), + 'required' => !$this->object()->isApplyRule(), + 'description' => $this->translate( + 'Name for the instance you are going to create' + ) + ]); + $this->groupMainProperties()->setButtons(); + } + + protected function isNew() + { + return $this->object === null; + } + + protected function deleteObject($object) + { + } + + protected function getObjectClassname() + { + return IcingaService::class; + } + + public function succeeded() + { + return $this->succeeded; + } + + public function onSuccess() + { + $this->succeeded = true; + } +} diff --git a/library/Director/DataType/DataTypeDictionary.php b/library/Director/DataType/DataTypeDictionary.php new file mode 100644 index 00000000..ff0cf276 --- /dev/null +++ b/library/Director/DataType/DataTypeDictionary.php @@ -0,0 +1,107 @@ +getObject(); + if ($form->isTemplate()) { + return $form->createElement('simpleNote', $name, [ + 'ignore' => true, + 'value' => Html::tag('span', $form->translate('To be managed on objects only')), + ]); + } + if (! $object->hasBeenLoadedFromDb()) { + return $form->createElement('simpleNote', $name, [ + 'ignore' => true, + 'value' => Html::tag( + 'span', + $form->translate('Can be managed once this object has been created') + ), + ]); + } + $params = [ + 'varname' => substr($name, 4), + ]; + if ($object instanceof IcingaHost) { + $params['host'] = $object->getObjectName(); + } elseif ($object instanceof IcingaService) { + $params['host'] = $object->get('host'); + $params['service'] = $object->getObjectName(); + } + return $form->createElement('InstanceSummary', $name, [ + 'linkParams' => $params + ]); + } + + public static function addSettingsFormFields(QuickForm $form) + { + /** @var DirectorObjectForm $form */ + $db = $form->getDb()->getDbAdapter(); + $enum = [ + 'host' => $form->translate('Hosts'), + 'service' => $form->translate('Services'), + ]; + + $form->addElement('select', 'template_object_type', [ + 'label' => $form->translate('Template (Object) Type'), + 'description' => $form->translate( + 'Please choose a specific Icinga object type. All ' + ), + 'class' => 'autosubmit', + 'required' => true, + 'multiOptions' => $form->optionalEnum($enum), + 'sorted' => true, + ]); + + // There should be a helper method for this + if ($form->hasBeenSent()) { + $type = $form->getSentOrObjectValue('template_object_type'); + } else { + $type = $form->getObject()->getSetting('template_object_type'); + } + if (empty($type)) { + return $form; + } + + if (array_key_exists($type, $enum)) { + $form->addElement('select', 'template_name', [ + 'label' => $form->translate('Template'), + 'multiOptions' => $form->optionalEnum(self::fetchTemplateNames($db, $type)), + 'required' => true, + ]); + } else { + throw new RuntimeException("$type is not a valid Dictionary object type"); + } + + return $form; + } + + protected static function fetchTemplateNames($db, $type) + { + $query = $db->select() + ->from("icinga_$type", ['a' => 'object_name', 'b' => 'object_name']) + ->where('object_type = ?', 'template') + ->where('template_choice_id IS NULL') + ->order('object_name'); + + return $db->fetchPairs($query); + } +} diff --git a/library/Director/Web/Form/Element/InstanceSummary.php b/library/Director/Web/Form/Element/InstanceSummary.php new file mode 100644 index 00000000..722ad26e --- /dev/null +++ b/library/Director/Web/Form/Element/InstanceSummary.php @@ -0,0 +1,51 @@ +instances = $value; + return $this; + } + + public function getValue() + { + return Html::tag('span', [ + Html::tag('italic', 'empty'), + ' ', + Link::create('Manage Instances', 'director/data/dictionary', $this->linkParams, [ + 'data-base-target' => '_next', + 'class' => 'icon-forward' + ]) + ]); + } + + public function isValid($value, $context = null) + { + return true; + } +} diff --git a/register-hooks.php b/register-hooks.php index 9f937b88..1ba99ecb 100644 --- a/register-hooks.php +++ b/register-hooks.php @@ -5,6 +5,7 @@ use Icinga\Module\Director\DataType\DataTypeArray; use Icinga\Module\Director\DataType\DataTypeBoolean; use Icinga\Module\Director\DataType\DataTypeDatalist; use Icinga\Module\Director\DataType\DataTypeDirectorObject; +use Icinga\Module\Director\DataType\DataTypeDictionary; use Icinga\Module\Director\DataType\DataTypeNumber; use Icinga\Module\Director\DataType\DataTypeSqlQuery; use Icinga\Module\Director\DataType\DataTypeString; @@ -67,6 +68,7 @@ $directorHooks = [ DataTypeArray::class, DataTypeBoolean::class, DataTypeDatalist::class, + DataTypeDictionary::class, DataTypeNumber::class, DataTypeDirectorObject::class, DataTypeSqlQuery::class,