DataTypeDictionary: new data type

fixes #337
This commit is contained in:
Thomas Gelf 2021-01-15 11:45:35 +01:00
parent 6687524d2f
commit 40e1b5a798
5 changed files with 436 additions and 0 deletions

View File

@ -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(

View File

@ -0,0 +1,54 @@
<?php
namespace Icinga\Module\Director\Forms;
use Icinga\Module\Director\Web\Form\DirectorObjectForm;
use Icinga\Module\Director\Objects\IcingaService;
class IcingaServiceDictionaryMemberForm extends DirectorObjectForm
{
/** @var IcingaService */
protected $object;
private $succeeded;
/**
* @throws \Zend_Form_Exception
*/
public function setup()
{
$this->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;
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace Icinga\Module\Director\DataType;
use Icinga\Module\Director\Hook\DataTypeHook;
use Icinga\Module\Director\Objects\IcingaHost;
use Icinga\Module\Director\Objects\IcingaService;
use Icinga\Module\Director\Web\Form\DirectorObjectForm;
use Icinga\Module\Director\Web\Form\QuickForm;
use InvalidArgumentException;
use ipl\Html\Html;
use RuntimeException;
class DataTypeDictionary extends DataTypeHook
{
public function getFormElement($name, QuickForm $form)
{
if (strpos($name, 'var_') !== 0) {
throw new InvalidArgumentException(
"'$name' is not a valid candidate for a Nested Dictionary, 'var_*' expected"
);
}
/** @var DirectorObjectForm $form */
$object = $form->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);
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Icinga\Module\Director\Web\Form\Element;
use gipfl\IcingaWeb2\Link;
use ipl\Html\Html;
/**
* Used by the
*/
class InstanceSummary extends FormElement
{
public $helper = 'formSimpleNote';
/**
* Always ignore this element
* @codingStandardsIgnoreStart
*
* @var boolean
*/
protected $_ignore = true;
// @codingStandardsIgnoreEnd
private $instances;
/** @var array will be set via options */
protected $linkParams;
public function setValue($value)
{
$this->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;
}
}

View File

@ -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,