mirror of
https://github.com/Icinga/icingaweb2-module-director.git
synced 2025-09-21 08:57:41 +02:00
Alternative custom property support
WIP: ipl form for custom properties DirectorObjectForm: Do not show custom variables for hosts Show inherited values in placeholder Fixes the instantiable dictionary loading in host objects, and adds a duplicate save button to fix the storing of custom properties. Renaming of variables for better readability.
This commit is contained in:
parent
6749b4fb08
commit
c4f3792d2f
@ -4,6 +4,9 @@ namespace Icinga\Module\Director\Controllers;
|
||||
|
||||
use gipfl\Web\Widget\Hint;
|
||||
use Icinga\Module\Director\Auth\Permission;
|
||||
use Icinga\Module\Director\CustomVariable\CustomVariables;
|
||||
use Icinga\Module\Director\Exception\NestingError;
|
||||
use Icinga\Module\Director\Forms\CustomPropertiesForm;
|
||||
use Icinga\Module\Director\Integration\Icingadb\IcingadbBackend;
|
||||
use Icinga\Module\Director\Integration\MonitoringModule\Monitoring;
|
||||
use Icinga\Module\Director\Web\Table\ObjectsTableService;
|
||||
@ -12,7 +15,6 @@ use gipfl\IcingaWeb2\Link;
|
||||
use gipfl\IcingaWeb2\Url;
|
||||
use gipfl\IcingaWeb2\Widget\Tabs;
|
||||
use Exception;
|
||||
use Icinga\Module\Director\CustomVariable\CustomVariableDictionary;
|
||||
use Icinga\Module\Director\Db\AppliedServiceSetLoader;
|
||||
use Icinga\Module\Director\DirectorObject\Lookup\ServiceFinder;
|
||||
use Icinga\Module\Director\Forms\IcingaAddServiceForm;
|
||||
@ -25,9 +27,10 @@ use Icinga\Module\Director\Restriction\HostgroupRestriction;
|
||||
use Icinga\Module\Director\Repository\IcingaTemplateRepository;
|
||||
use Icinga\Module\Director\Web\Controller\ObjectController;
|
||||
use Icinga\Module\Director\Web\SelfService;
|
||||
use Icinga\Module\Director\Web\Table\IcingaHostAppliedForServiceTable;
|
||||
use Icinga\Module\Director\Web\Table\IcingaHostAppliedServicesTable;
|
||||
use Icinga\Module\Director\Web\Table\IcingaServiceSetServiceTable;
|
||||
use ipl\Web\Widget\ButtonLink;
|
||||
use PDO;
|
||||
|
||||
class HostController extends ObjectController
|
||||
{
|
||||
@ -89,6 +92,115 @@ class HostController extends ObjectController
|
||||
$this->addOptionalMonitoringLink();
|
||||
}
|
||||
|
||||
public function variablesAction()
|
||||
{
|
||||
$this->assertPermission('director/admin');
|
||||
$object = $this->requireObject();
|
||||
$type = $this->getType();
|
||||
|
||||
$this->addTitle(
|
||||
$this->translate('Custom Variables: %s'),
|
||||
$object->getObjectName()
|
||||
);
|
||||
|
||||
$objectProperties = $this->getObjectCustomProperties();
|
||||
if ($this->object->isTemplate()) {
|
||||
$this->actions()->add(
|
||||
(new ButtonLink(
|
||||
$this->translate('Add Property'),
|
||||
Url::fromPath('director/host/add-property', ['uuid' => $this->getUuidFromUrl()])->getAbsoluteUrl(),
|
||||
null,
|
||||
['class' => 'control-button']
|
||||
))->openInModal()
|
||||
);
|
||||
|
||||
if ($objectProperties) {
|
||||
$this->actions()->add(
|
||||
(new ButtonLink(
|
||||
$this->translate('Remove Property'),
|
||||
Url::fromPath(
|
||||
'director/host/remove-property',
|
||||
['uuid' => $this->getUuidFromUrl()]
|
||||
)->getAbsoluteUrl(),
|
||||
null,
|
||||
['class' => 'control-button']
|
||||
))->openInModal()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$vars = json_decode(json_encode($this->object->getVars()), true);
|
||||
|
||||
if ($objectProperties) {
|
||||
$form = (new CustomPropertiesForm($this->db(), $object, $objectProperties))
|
||||
->populate($vars)
|
||||
->on(CustomPropertiesForm::ON_SUCCESS, function () {
|
||||
$this->redirectNow('__REFRESH__');
|
||||
})
|
||||
->handleRequest($this->getServerRequest());
|
||||
|
||||
try {
|
||||
$this->content()->add($form);
|
||||
} catch (NestingError $e) {
|
||||
$this->content()->add(Hint::error($e->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
$this->tabs()->activate('variables');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom properties for the host.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getObjectCustomProperties(): array
|
||||
{
|
||||
if ($this->object->uuid === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$type = $this->object->getShortTableName();
|
||||
|
||||
$parents = $this->object->listAncestorIds();
|
||||
|
||||
$uuids = [];
|
||||
$db = $this->db();
|
||||
foreach ($parents as $parent) {
|
||||
$uuids[] = IcingaHost::load($parent, $db)->get('uuid');
|
||||
}
|
||||
|
||||
$uuids[] = $this->object->get('uuid');
|
||||
$query = $db->getDbAdapter()
|
||||
->select()
|
||||
->from(
|
||||
['dp' => 'director_property'],
|
||||
[
|
||||
'key_name' => 'dp.key_name',
|
||||
'uuid' => 'dp.uuid',
|
||||
'value_type' => 'dp.value_type',
|
||||
'label' => 'dp.label',
|
||||
'instantiable' => 'dp.instantiable',
|
||||
'required' => 'iop.required',
|
||||
'children' => 'COUNT(cdp.uuid)'
|
||||
]
|
||||
)
|
||||
->join(['iop' => "icinga_$type" . '_property'], 'dp.uuid = iop.property_uuid', [])
|
||||
->joinLeft(['cdp' => 'director_property'], 'cdp.parent_uuid = dp.uuid', [])
|
||||
->where('iop.' . $type . '_uuid IN (?)', $uuids)
|
||||
->group(['dp.uuid', 'dp.key_name', 'dp.value_type', 'dp.label', 'dp.instantiable', 'iop.required'])
|
||||
->order('children')
|
||||
->order('instantiable')
|
||||
->order('key_name');
|
||||
|
||||
$result = [];
|
||||
foreach ($db->getDbAdapter()->fetchAll($query, fetchMode: PDO::FETCH_ASSOC) as $row) {
|
||||
$result[$row['key_name']] = $row;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function serviceAction()
|
||||
{
|
||||
$host = $this->getHostObject();
|
||||
@ -99,7 +211,7 @@ class HostController extends ObjectController
|
||||
->setBranch($this->getBranch())
|
||||
->setHost($host)
|
||||
->setDb($this->db())
|
||||
->handleRequest()
|
||||
->handleRequest(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -114,7 +226,7 @@ class HostController extends ObjectController
|
||||
->setBranch($this->getBranch())
|
||||
->setHost($host)
|
||||
->setDb($this->db())
|
||||
->handleRequest()
|
||||
->handleRequest(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -128,12 +240,12 @@ class HostController extends ObjectController
|
||||
$this->translate('Add service'),
|
||||
'director/host/service',
|
||||
['name' => $hostname],
|
||||
['class' => 'icon-plus']
|
||||
['class' => 'icon-plus'],
|
||||
))->add(Link::create(
|
||||
$this->translate('Add service set'),
|
||||
'director/host/serviceset',
|
||||
['name' => $hostname],
|
||||
['class' => 'icon-plus']
|
||||
['class' => 'icon-plus'],
|
||||
));
|
||||
}
|
||||
|
||||
@ -159,7 +271,7 @@ class HostController extends ObjectController
|
||||
} elseif ($auth->hasPermission($this->getServicesReadOnlyPermission())) {
|
||||
$redirectUrl = Url::fromPath('director/host/servicesro', [
|
||||
'name' => $hostName,
|
||||
'service' => $serviceName
|
||||
'service' => $serviceName,
|
||||
]);
|
||||
} else {
|
||||
$redirectUrl = Url::fromPath('director/host/invalidservice', [
|
||||
@ -179,7 +291,7 @@ class HostController extends ObjectController
|
||||
if (! $this->showInfoForNonDirectorService()) {
|
||||
$this->content()->add(Hint::error(sprintf(
|
||||
$this->translate('No such service: %s'),
|
||||
$this->params->get('service')
|
||||
$this->params->get('service'),
|
||||
)));
|
||||
}
|
||||
|
||||
@ -199,7 +311,7 @@ class HostController extends ObjectController
|
||||
'The configuration for this object has not been rendered by'
|
||||
. ' Icinga Director. You can find it on line %s in %s.',
|
||||
Html::tag('strong', null, $source->first_line),
|
||||
Html::tag('strong', null, $source->path)
|
||||
Html::tag('strong', null, $source->path),
|
||||
)));
|
||||
}
|
||||
}
|
||||
@ -245,8 +357,8 @@ class HostController extends ObjectController
|
||||
$content->add(
|
||||
$table->setTitle(sprintf(
|
||||
$this->translate('Inherited from %s'),
|
||||
$parent->getObjectName()
|
||||
))
|
||||
$parent->getObjectName(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -268,7 +380,7 @@ class HostController extends ObjectController
|
||||
->setBranch($branch)
|
||||
->setAffectedHost($host)
|
||||
->setTitle($title)
|
||||
->removeQueryLimit()
|
||||
->removeQueryLimit(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -326,8 +438,8 @@ class HostController extends ObjectController
|
||||
$content->add(
|
||||
$table->setTitle(sprintf(
|
||||
'Inherited from %s',
|
||||
$parent->getObjectName()
|
||||
))
|
||||
$parent->getObjectName(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -348,7 +460,7 @@ class HostController extends ObjectController
|
||||
->setAffectedHost($host)
|
||||
->setReadonly()
|
||||
->highlightService($service)
|
||||
->setTitle($title)
|
||||
->setTitle($title),
|
||||
);
|
||||
}
|
||||
|
||||
@ -379,15 +491,15 @@ class HostController extends ObjectController
|
||||
$query = $db->getDbAdapter()->select()
|
||||
->from(
|
||||
array('ss' => 'icinga_service_set'),
|
||||
'ss.*'
|
||||
'ss.*',
|
||||
)->join(
|
||||
array('hsi' => 'icinga_service_set_inheritance'),
|
||||
'hsi.parent_service_set_id = ss.id',
|
||||
array()
|
||||
array(),
|
||||
)->join(
|
||||
array('hs' => 'icinga_service_set'),
|
||||
'hs.id = hsi.service_set_id',
|
||||
array()
|
||||
array(),
|
||||
)->where('hs.host_id = ?', $host->get('id'));
|
||||
|
||||
$sets = IcingaServiceSet::loadAll($db, $query, 'object_name');
|
||||
@ -428,7 +540,7 @@ class HostController extends ObjectController
|
||||
|
||||
$this->addTitle(
|
||||
$this->translate('Applied service: %s'),
|
||||
$serviceName
|
||||
$serviceName,
|
||||
);
|
||||
|
||||
$this->content()->add(
|
||||
@ -438,7 +550,7 @@ class HostController extends ObjectController
|
||||
->setHost($host)
|
||||
->setApplyGenerated($parent)
|
||||
->setObject($service)
|
||||
->handleRequest()
|
||||
->handleRequest(),
|
||||
);
|
||||
|
||||
$this->commonForServices();
|
||||
@ -456,7 +568,7 @@ class HostController extends ObjectController
|
||||
|
||||
$parent = IcingaService::load([
|
||||
'object_name' => $serviceName,
|
||||
'host_id' => $from->get('id')
|
||||
'host_id' => $from->get('id'),
|
||||
], $this->db());
|
||||
|
||||
// TODO: we want to eventually show the host template name, doesn't work
|
||||
@ -493,21 +605,21 @@ class HostController extends ObjectController
|
||||
$db = $this->db()->getDbAdapter();
|
||||
$query = $db->select()->from(
|
||||
array('ss' => 'icinga_service_set'),
|
||||
array('id' => 'ss.id')
|
||||
array('id' => 'ss.id'),
|
||||
)->join(
|
||||
array('si' => 'icinga_service_set_inheritance'),
|
||||
'si.service_set_id = ss.id',
|
||||
array()
|
||||
array(),
|
||||
)->where(
|
||||
'si.parent_service_set_id = ?',
|
||||
$this->params->get('setId')
|
||||
$this->params->get('setId'),
|
||||
)->where('ss.host_id = ?', $this->object->get('id'));
|
||||
|
||||
IcingaServiceSet::loadWithAutoIncId($db->fetchOne($query), $this->db())->delete();
|
||||
$this->redirectNow(
|
||||
Url::fromPath('director/host/services', array(
|
||||
'name' => $this->object->getObjectName()
|
||||
))
|
||||
'name' => $this->object->getObjectName(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -521,7 +633,7 @@ class HostController extends ObjectController
|
||||
$serviceName = $this->params->get('service');
|
||||
$setParams = [
|
||||
'object_name' => $this->params->get('set'),
|
||||
'host_id' => $host->get('id')
|
||||
'host_id' => $host->get('id'),
|
||||
];
|
||||
$setTemplate = IcingaServiceSet::load($this->params->get('set'), $db);
|
||||
if (IcingaServiceSet::exists($setParams, $db)) {
|
||||
@ -532,7 +644,7 @@ class HostController extends ObjectController
|
||||
|
||||
$service = IcingaService::load([
|
||||
'object_name' => $serviceName,
|
||||
'service_set_id' => $setTemplate->get('id')
|
||||
'service_set_id' => $setTemplate->get('id'),
|
||||
], $this->db());
|
||||
$service = IcingaService::create([
|
||||
'id' => $service->get('id'),
|
||||
@ -548,7 +660,7 @@ class HostController extends ObjectController
|
||||
$this->translate('%s on %s (from set: %s)'),
|
||||
$serviceName,
|
||||
$host->getObjectName(),
|
||||
$set->getObjectName()
|
||||
$set->getObjectName(),
|
||||
);
|
||||
|
||||
$form = IcingaServiceForm::load()
|
||||
@ -570,7 +682,7 @@ class HostController extends ObjectController
|
||||
$this->translate('back'),
|
||||
'director/host/services',
|
||||
['name' => $host->getObjectName()],
|
||||
['class' => 'icon-left-big']
|
||||
['class' => 'icon-left-big'],
|
||||
));
|
||||
$this->tabs()->activate('services');
|
||||
}
|
||||
@ -605,8 +717,8 @@ class HostController extends ObjectController
|
||||
$this->translate('Show'),
|
||||
$backend->getHostUrl($host->getObjectName()),
|
||||
null,
|
||||
['class' => 'icon-globe critical', 'data-base-target' => '_next']
|
||||
)
|
||||
['class' => 'icon-globe critical', 'data-base-target' => '_next'],
|
||||
),
|
||||
);
|
||||
|
||||
// Intentionally placed here, show it only for deployed Hosts
|
||||
@ -629,12 +741,12 @@ class HostController extends ObjectController
|
||||
[
|
||||
'type' => 'host',
|
||||
'plural' => 'hosts',
|
||||
'name' => $this->object->getObjectName()
|
||||
'name' => $this->object->getObjectName(),
|
||||
],
|
||||
[
|
||||
'class' => 'icon-zoom-in',
|
||||
'data-base-target' => '_next'
|
||||
]
|
||||
'data-base-target' => '_next',
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
|
67
application/controllers/PropertiesController.php
Normal file
67
application/controllers/PropertiesController.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Icinga\Module\Director\Controllers;
|
||||
|
||||
use Icinga\Application\Config;
|
||||
use Icinga\Module\Director\Db;
|
||||
use Icinga\Module\Director\Forms\PropertyForm;
|
||||
use Icinga\Module\Director\Web\Widget\PropertyTable;
|
||||
use Icinga\Web\Notification;
|
||||
use ipl\Html\Html;
|
||||
use ipl\Html\Text;
|
||||
use ipl\Web\Compat\CompatController;
|
||||
use ipl\Web\Url;
|
||||
use ipl\Web\Widget\ButtonLink;
|
||||
|
||||
class PropertiesController extends CompatController
|
||||
{
|
||||
public function indexAction()
|
||||
{
|
||||
$this->addTitleTab($this->translate('Properties'));
|
||||
|
||||
$db = Db::fromResourceName(
|
||||
Config::module('director')->get('db', 'resource')
|
||||
)->getDbAdapter();
|
||||
|
||||
$query = $db->select()
|
||||
->from('director_property')
|
||||
->where('parent_uuid IS NULL')
|
||||
->order('key_name');
|
||||
|
||||
$properties = new PropertyTable($db->fetchAll($query));
|
||||
|
||||
$this->addControl(Html::tag('div', ['class' => 'property-form'], [
|
||||
(new ButtonLink(
|
||||
[Text::create('Add property')],
|
||||
Url::fromPath('director/properties/add'),
|
||||
null,
|
||||
[
|
||||
'class' => 'control-button'
|
||||
]
|
||||
))->setBaseTarget('_next')
|
||||
]));
|
||||
|
||||
$this->addContent($properties);
|
||||
}
|
||||
|
||||
public function addAction()
|
||||
{
|
||||
$this->addTitleTab($this->translate('Add property'));
|
||||
$db = Db::fromResourceName(
|
||||
Config::module('director')->get('db', 'resource')
|
||||
);
|
||||
|
||||
$propertyForm = (new PropertyForm($db))
|
||||
->on(PropertyForm::ON_SUCCESS, function (PropertyForm $form) {
|
||||
Notification::success(sprintf(
|
||||
$this->translate('Property "%s" has successfully been added'),
|
||||
$form->getValue('key_name')
|
||||
));
|
||||
|
||||
$this->redirectNow(Url::fromPath('director/property', ['uuid' => $form->getUUid()->toString()]));
|
||||
})
|
||||
->handleRequest($this->getServerRequest());
|
||||
|
||||
$this->addContent($propertyForm);
|
||||
}
|
||||
}
|
241
application/controllers/PropertyController.php
Normal file
241
application/controllers/PropertyController.php
Normal file
@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
namespace Icinga\Module\Director\Controllers;
|
||||
|
||||
use Icinga\Application\Config;
|
||||
use Icinga\Module\Director\Db;
|
||||
use Icinga\Module\Director\Forms\PropertyForm;
|
||||
use Icinga\Module\Director\Web\Widget\PropertyTable;
|
||||
use Icinga\Web\Notification;
|
||||
use ipl\Html\HtmlElement;
|
||||
use ipl\Html\Text;
|
||||
use ipl\Web\Compat\CompatController;
|
||||
use ipl\Web\Url;
|
||||
use ipl\Web\Widget\ButtonLink;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
use Zend_Db;
|
||||
|
||||
class PropertyController extends CompatController
|
||||
{
|
||||
/** @var Db */
|
||||
protected $db;
|
||||
|
||||
public function init()
|
||||
{
|
||||
parent::init();
|
||||
|
||||
$this->db = Db::fromResourceName(
|
||||
Config::module('director')->get('db', 'resource')
|
||||
);
|
||||
}
|
||||
|
||||
public function indexAction()
|
||||
{
|
||||
$uuid = $this->params->shiftRequired('uuid');
|
||||
$uuid = Uuid::fromString($uuid);
|
||||
$db = $this->db->getDbAdapter();
|
||||
$property = $this->fetchProperty($uuid);
|
||||
|
||||
if ($property['value_type'] === 'array' && $property['instantiable'] === 'y') {
|
||||
$itemTypeQuery = $db
|
||||
->select()->from('director_property', 'value_type')
|
||||
->where(
|
||||
'parent_uuid = ? AND key_name = \'0\'',
|
||||
$uuid->getBytes()
|
||||
);
|
||||
|
||||
$property['item_type'] = $db->fetchOne($itemTypeQuery);
|
||||
}
|
||||
|
||||
$showFields = ($property['value_type'] === 'array' && $property['instantiable'] !== 'y')
|
||||
|| $property['value_type'] === 'dict';
|
||||
$propertyForm = (new PropertyForm($this->db, $uuid))
|
||||
->populate($property)
|
||||
->setAction(Url::fromRequest()->getAbsoluteUrl())
|
||||
->on(PropertyForm::ON_SENT, function (PropertyForm $form) use (&$showFields) {
|
||||
$values = $form->getValues();
|
||||
$showFields = ($values['value_type'] === 'array' && $values['instantiable'] !== 'y')
|
||||
|| $values['value_type'] === 'dict';
|
||||
})
|
||||
->on(PropertyForm::ON_SUCCESS, function (PropertyForm $form) {
|
||||
if ($form->getPressedSubmitElement()->getName() === 'delete') {
|
||||
Notification::success(sprintf(
|
||||
$this->translate('Property "%s" has successfully been deleted'),
|
||||
$form->getValue('key_name')
|
||||
));
|
||||
|
||||
$this->redirectNow('__CLOSE__');
|
||||
} else {
|
||||
Notification::success(sprintf(
|
||||
$this->translate('Property "%s" has successfully been saved'),
|
||||
$form->getValue('key_name')
|
||||
));
|
||||
|
||||
$this->sendExtraUpdates(['#col1']);
|
||||
|
||||
$this->redirectNow(
|
||||
Url::fromPath('director/property', ['uuid' => $form->getUUid()->toString()])
|
||||
);
|
||||
}
|
||||
})
|
||||
->handleRequest($this->getServerRequest());
|
||||
|
||||
$this->addContent($propertyForm);
|
||||
|
||||
if ($showFields) {
|
||||
$this->addContent(new HtmlElement('h2', null, Text::create($this->translate('Fields'))));
|
||||
$button = (new ButtonLink(
|
||||
Text::create($this->translate('Add Field')),
|
||||
Url::fromPath('director/property/add-field', [
|
||||
'uuid' => $uuid->toString()
|
||||
]),
|
||||
null,
|
||||
['class' => 'control-button']
|
||||
))->openInModal();
|
||||
|
||||
$fieldQuery = $db
|
||||
->select()
|
||||
->from('director_property')
|
||||
->where('parent_uuid = ?', $uuid->getBytes())
|
||||
->order('key_name');
|
||||
|
||||
$this->addContent($button);
|
||||
|
||||
$fields = new PropertyTable($db->fetchAll($fieldQuery), true);
|
||||
$this->addContent($fields);
|
||||
}
|
||||
|
||||
$this->addTitleTab($this->translate('Property') . ': ' . $property['key_name']);
|
||||
}
|
||||
|
||||
public function addFieldAction()
|
||||
{
|
||||
$uuid = $this->params->shiftRequired('uuid');
|
||||
$this->addTitleTab($this->translate('Add Field'));
|
||||
$uuid = Uuid::fromString($uuid);
|
||||
|
||||
$parent = $this->fetchProperty($uuid);
|
||||
$hideKeyNameField = $parent['value_type'] === 'array'
|
||||
&& $parent['instantiable'] === 'n';
|
||||
|
||||
$propertyForm = (new PropertyForm($this->db, null, true, $uuid))
|
||||
->setHideKeyNameElement($hideKeyNameField)
|
||||
->setIsNestedField($parent['parent_uuid'] !== null)
|
||||
->setAction(Url::fromRequest()->getAbsoluteUrl())
|
||||
->on(PropertyForm::ON_SUCCESS, function (PropertyForm $form) {
|
||||
Notification::success(sprintf(
|
||||
$this->translate('Property "%s" has successfully been saved'),
|
||||
$form->getValue('key_name')
|
||||
));
|
||||
|
||||
$this->sendExtraUpdates(['#col1']);
|
||||
$this->redirectNow(
|
||||
Url::fromPath('director/property', ['uuid' => $form->getParentUUid()->toString()])
|
||||
);
|
||||
})
|
||||
->handleRequest($this->getServerRequest());
|
||||
|
||||
$this->addContent($propertyForm);
|
||||
}
|
||||
|
||||
public function editFieldAction()
|
||||
{
|
||||
$uuid = Uuid::fromString($this->params->shiftRequired('uuid'));
|
||||
$parentUuid = Uuid::fromString($this->params->shiftRequired('parent_uuid'));
|
||||
|
||||
$parent = $this->fetchProperty($parentUuid);
|
||||
$hideKeyNameField = $parent['value_type'] === 'array'
|
||||
&& $parent['instantiable'] === 'n';
|
||||
|
||||
$property = $this->fetchProperty($uuid);
|
||||
$db = $this->db->getDbAdapter();
|
||||
|
||||
if ($property['value_type'] === 'array' && $property['instantiable'] === 'y') {
|
||||
$itemTypeQuery = $db
|
||||
->select()->from('director_property', 'value_type')
|
||||
->where(
|
||||
'parent_uuid = ? AND key_name = \'0\'',
|
||||
$uuid->getBytes()
|
||||
);
|
||||
|
||||
$property['item_type'] = $db->fetchOne($itemTypeQuery);
|
||||
}
|
||||
|
||||
$this->addTitleTab(sprintf($this->translate('Edit Field: %s'), $property['key_name']));
|
||||
|
||||
$showFields = ($property['value_type'] === 'array' && $property['instantiable'] !== 'y')
|
||||
|| $property['value_type'] === 'dict';
|
||||
$propertyForm = (new PropertyForm($this->db, $uuid, true, $parentUuid))
|
||||
->setHideKeyNameElement($hideKeyNameField)
|
||||
->setIsNestedField($parent['parent_uuid'] !== null)
|
||||
->populate($property)
|
||||
->setAction(Url::fromRequest()->getAbsoluteUrl())
|
||||
->on(PropertyForm::ON_SENT, function (PropertyForm $form) use (&$showFields) {
|
||||
$values = $form->getValues();
|
||||
$showFields = ($values['value_type'] === 'array' && $values['instantiable'] !== 'y')
|
||||
|| $values['value_type'] === 'dict';
|
||||
})
|
||||
->on(PropertyForm::ON_SUCCESS, function (PropertyForm $form) {
|
||||
if ($form->getPressedSubmitElement()->getName() === 'delete') {
|
||||
Notification::success(sprintf(
|
||||
$this->translate('Property "%s" has successfully been deleted'),
|
||||
$form->getValue('key_name')
|
||||
));
|
||||
|
||||
$this->redirectNow('__CLOSE__');
|
||||
} else {
|
||||
Notification::success(sprintf(
|
||||
$this->translate('Property "%s" has successfully been saved'),
|
||||
$form->getValue('key_name')
|
||||
));
|
||||
|
||||
$this->redirectNow(Url::fromRequest()->getAbsoluteUrl());
|
||||
}
|
||||
})
|
||||
->handleRequest($this->getServerRequest());
|
||||
|
||||
$this->addContent($propertyForm);
|
||||
|
||||
if ($showFields) {
|
||||
$this->addContent(new HtmlElement('h2', null, Text::create($this->translate('Fields'))));
|
||||
$button = (new ButtonLink(
|
||||
Text::create($this->translate('Add Field')),
|
||||
Url::fromPath('director/property/add-field', [
|
||||
'uuid' => $uuid->toString()
|
||||
]),
|
||||
null,
|
||||
['class' => 'control-button']
|
||||
))->openInModal();
|
||||
|
||||
$fieldQuery = $db
|
||||
->select()
|
||||
->from('director_property')
|
||||
->where('parent_uuid = ?', $uuid->getBytes())
|
||||
->order('key_name');
|
||||
|
||||
$this->addContent($button);
|
||||
|
||||
$fields = new PropertyTable($db->fetchAll($fieldQuery), true);
|
||||
$this->addContent($fields);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch property for the given UUID
|
||||
*
|
||||
* @param UuidInterface $uuid UUID of the given property
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function fetchProperty(UuidInterface $uuid): array
|
||||
{
|
||||
$db = $this->db->getDbAdapter();
|
||||
|
||||
$query = $db
|
||||
->select()->from('director_property')
|
||||
->where('uuid = ?', $uuid->getBytes());
|
||||
|
||||
return $db->fetchRow($query, [], Zend_Db::FETCH_ASSOC);
|
||||
}
|
||||
}
|
490
application/forms/CustomPropertiesForm.php
Normal file
490
application/forms/CustomPropertiesForm.php
Normal file
@ -0,0 +1,490 @@
|
||||
<?php
|
||||
|
||||
namespace Icinga\Module\Director\Forms;
|
||||
|
||||
use Icinga\Module\Director\Data\Db\DbConnection;
|
||||
use Icinga\Module\Director\Objects\IcingaObject;
|
||||
use Icinga\Module\Director\Web\Form\Element\ArrayElement;
|
||||
use Icinga\Module\Director\Web\Form\Element\IplBoolean;
|
||||
use Icinga\Web\Notification;
|
||||
use Icinga\Web\Session;
|
||||
use ipl\Html\FormElement\FieldsetElement;
|
||||
use ipl\Html\FormElement\SubmitButtonElement;
|
||||
use ipl\Html\FormElement\SubmitElement;
|
||||
use ipl\I18n\Translation;
|
||||
use ipl\Web\Common\CsrfCounterMeasure;
|
||||
use ipl\Web\Compat\CompatForm;
|
||||
use ipl\Web\FormElement\TermInput;
|
||||
use ipl\Web\Widget\Icon;
|
||||
use PDO;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
class CustomPropertiesForm extends CompatForm
|
||||
{
|
||||
use CsrfCounterMeasure;
|
||||
use Translation;
|
||||
|
||||
public function __construct(
|
||||
public readonly DbConnection $db,
|
||||
public readonly IcingaObject $object,
|
||||
protected array $objectProperties = []
|
||||
) {
|
||||
$this->addAttributes(['class' => ['custom-properties-form']]);
|
||||
}
|
||||
|
||||
protected function assemble(): void
|
||||
{
|
||||
$this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId()));
|
||||
$inheritedVars = json_decode(json_encode($this->object->getInheritedVars()), JSON_OBJECT_AS_ARRAY);
|
||||
$origins = $this->object->getOriginsVars();
|
||||
|
||||
/** @var SubmitElement $submitButton */
|
||||
$submitButton = $this->createElement('submit', 'save', [
|
||||
'label' => $this->translate('Save')
|
||||
]);
|
||||
|
||||
$this->registerElement($submitButton);
|
||||
|
||||
$duplicateSubmit = $this->duplicateSubmitButton($submitButton);
|
||||
|
||||
$this->addElement($duplicateSubmit);
|
||||
|
||||
foreach ($this->objectProperties as $objectProperty) {
|
||||
$inheritedVar = [];
|
||||
if (isset($inheritedVars[$objectProperty['key_name']])) {
|
||||
$inheritedVar = [$inheritedVars[$objectProperty['key_name']], $origins->{$objectProperty['key_name']}];
|
||||
}
|
||||
|
||||
$this->preparePropertyElement($objectProperty, inheritedValue: $inheritedVar);
|
||||
}
|
||||
|
||||
$this->addElement(
|
||||
$submitButton
|
||||
);
|
||||
}
|
||||
|
||||
protected function preparePropertyElement(
|
||||
array $objectProperty,
|
||||
FieldsetElement $parentElement = null,
|
||||
$inheritedValue = []
|
||||
): void {
|
||||
$isInstantiable = $objectProperty['instantiable'] === 'y';
|
||||
$fieldType = $this->fetchFieldType($objectProperty['value_type'], $isInstantiable);
|
||||
$placeholder = '';
|
||||
if ($inheritedValue && ! (is_array($inheritedValue[0]) || $objectProperty['value_type'] === 'bool')) {
|
||||
$placeholder = $inheritedValue[0]
|
||||
. sprintf($this->translate(' (inherited from "%s")'), $inheritedValue[1]);
|
||||
}
|
||||
|
||||
$fieldName = $objectProperty['key_name'];
|
||||
if ($parentElement) {
|
||||
$fieldName = is_numeric($fieldName)
|
||||
? 'item-' . $fieldName
|
||||
: $fieldName;
|
||||
}
|
||||
|
||||
if ($fieldType === 'boolean') {
|
||||
// $options = ['' => sprintf(' - %s - ', $this->translate('Please choose')), 'y' => 'Yes', 'n' => 'No'];
|
||||
|
||||
// if (! empty($inheritedValue)) {
|
||||
// $options[''] = ($inheritedValue[0] === 'y' ? 'Yes' : 'No')
|
||||
// . sprintf($this->translate(' (inherited from "%s")'), $inheritedValue[1]);
|
||||
// }
|
||||
|
||||
$field = new IplBoolean($fieldName, ['label' => $objectProperty['label'],'decorator' => ['ViewHelper']]);
|
||||
} elseif ($fieldType === 'extensibleSet') {
|
||||
$placeholder = ! empty($inheritedValue[0])
|
||||
? implode(', ', $inheritedValue[0])
|
||||
. sprintf($this->translate(' (inherited from "%s")'), $inheritedValue[1])
|
||||
: '';
|
||||
$field = (new ArrayElement($fieldName, [
|
||||
'label' => $objectProperty['label']
|
||||
]))
|
||||
->setPlaceholder($placeholder)
|
||||
->setVerticalTermDirection();
|
||||
} elseif ($fieldType === 'collection') {
|
||||
$field = new FieldsetElement($fieldName, [
|
||||
'label' => $objectProperty['label'],
|
||||
'class' => ['dictionary-element']
|
||||
]);
|
||||
} else {
|
||||
$field = $this->createElement(
|
||||
$fieldType,
|
||||
$fieldName,
|
||||
[
|
||||
'label' => $objectProperty['label'],
|
||||
'placeholder' => $placeholder
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if ($parentElement) {
|
||||
$parentElement->addElement($field);
|
||||
} else {
|
||||
$this->addElement($field);
|
||||
}
|
||||
|
||||
if ($field instanceof FieldsetElement) {
|
||||
$propertyItems = $this->fetchPropertyItems(Uuid::fromBytes($objectProperty['uuid']));
|
||||
|
||||
if (! $isInstantiable) {
|
||||
foreach ($propertyItems as $propertyItem) {
|
||||
$propertyInherited = [];
|
||||
if (isset($inheritedValue[0][$propertyItem['key_name']])) {
|
||||
$propertyInherited = [$inheritedValue[0][$propertyItem['key_name']], $inheritedValue[1]];
|
||||
}
|
||||
|
||||
$this->preparePropertyElement(
|
||||
$propertyItem,
|
||||
$field,
|
||||
$propertyInherited
|
||||
);
|
||||
}
|
||||
} elseif ($objectProperty['value_type'] === 'dict') {
|
||||
/** @var SubmitButtonElement $addItem */
|
||||
$addItem = $this->createElement(
|
||||
'submitButton',
|
||||
'add-item',
|
||||
[
|
||||
'label' => $this->translate('Add Item'),
|
||||
'formnovalidate' => true
|
||||
],
|
||||
);
|
||||
|
||||
$initialCountElement = $this->createElement(
|
||||
'hidden',
|
||||
'initial-count'
|
||||
);
|
||||
|
||||
$addedCountElement = $this->createElement(
|
||||
'hidden',
|
||||
'added-count',
|
||||
['class' => 'autosubmit'],
|
||||
);
|
||||
|
||||
$field->addElement($initialCountElement);
|
||||
$field->addElement($addedCountElement);
|
||||
$field->registerElement($addItem);
|
||||
$this->registerElement($field);
|
||||
|
||||
$addedItemsCount = (int) $addedCountElement->getValue();
|
||||
$initialItemsCount = (int) $initialCountElement->getValue();
|
||||
|
||||
$prefixElement = $this->createElement(
|
||||
'hidden',
|
||||
'prefixes'
|
||||
);
|
||||
|
||||
$field->addElement($prefixElement);
|
||||
|
||||
$prefixes = $prefixElement->getValue() !== null
|
||||
? explode(',', (string) $prefixElement->getValue())
|
||||
: [];
|
||||
|
||||
foreach ($prefixes as $idx => $prefix) {
|
||||
$propertyField = new FieldsetElement(
|
||||
'property-' . $prefix,
|
||||
['class' => ['dictionary-item', 'dictionary-element']]
|
||||
);
|
||||
$field->addElement($propertyField);
|
||||
|
||||
$propertyItemLabel = $this->createElement(
|
||||
'text',
|
||||
'label',
|
||||
[
|
||||
'label' => $this->translate('Item Label'),
|
||||
'required' => true,
|
||||
'class' => 'autosubmit'
|
||||
],
|
||||
);
|
||||
|
||||
$propertyField->addElement($propertyItemLabel);
|
||||
$field->registerElement($propertyField);
|
||||
$propertyField->setLabel($propertyItemLabel->getValue());
|
||||
foreach ($propertyItems as $propertyItem) {
|
||||
$inheritedVar = [];
|
||||
if (! empty($inheritedValue) && isset($inheritedValue[0][$propertyItemLabel->getValue()])) {
|
||||
$inheritedVar = [$inheritedValue[0][$propertyItemLabel->getValue()], $inheritedValue[1]];
|
||||
}
|
||||
|
||||
$this->preparePropertyElement(
|
||||
$propertyItem,
|
||||
$propertyField,
|
||||
$inheritedVar
|
||||
);
|
||||
}
|
||||
|
||||
/** @var SubmitButtonElement $removeItem */
|
||||
$removeItem = $this->createElement(
|
||||
'submitButton',
|
||||
"remove-item",
|
||||
[
|
||||
'class' => 'remove-button',
|
||||
'label' => new Icon('minus', ['title' => 'Remove item']),
|
||||
'value' => $prefix,
|
||||
'formnovalidate' => true
|
||||
]
|
||||
);
|
||||
|
||||
$propertyField->registerElement($removeItem);
|
||||
$propertyField->addHtml($removeItem);
|
||||
if ($removeItem->hasBeenPressed()) {
|
||||
$field->remove($propertyField);
|
||||
unset($prefixes[$idx]);
|
||||
$initialItemsCount -= 1;
|
||||
$initialCountElement->setValue($initialItemsCount);
|
||||
$prefixElement->setValue(implode(',', $prefixes));
|
||||
}
|
||||
}
|
||||
|
||||
if ($addItem->hasBeenPressed()) {
|
||||
$addedItemsCount += 1;
|
||||
$addedCountElement->setValue($addedItemsCount);
|
||||
}
|
||||
|
||||
$removedItems = 0;
|
||||
$removedItemIdx = null;
|
||||
for (
|
||||
$numberItem = $initialItemsCount;
|
||||
$numberItem < ($addedItemsCount + $initialItemsCount);
|
||||
$numberItem++
|
||||
) {
|
||||
$tempNumberItem = $numberItem;
|
||||
if ($removedItems > 0 && $removedItemIdx < $numberItem) {
|
||||
$tempNumberItem = $numberItem - 1;
|
||||
}
|
||||
|
||||
$idx = $tempNumberItem - $initialItemsCount;
|
||||
|
||||
$propertyField = new FieldsetElement('property-' . $tempNumberItem, [
|
||||
'label' => $this->translate('New Property') . " $idx",
|
||||
'class' => ['dictionary-item', 'dictionary-element']
|
||||
]);
|
||||
|
||||
$field->addElement($propertyField);
|
||||
$propertyItemLabel = $this->createElement(
|
||||
'text',
|
||||
'label',
|
||||
[
|
||||
'label' => $this->translate('Item Label'),
|
||||
'required' => true
|
||||
],
|
||||
);
|
||||
|
||||
$propertyField->addElement($propertyItemLabel);
|
||||
foreach ($propertyItems as $propertyItem) {
|
||||
$this->preparePropertyElement($propertyItem, $propertyField);
|
||||
}
|
||||
|
||||
$removeItem = $this->createElement(
|
||||
'submitButton',
|
||||
"remove-item",
|
||||
[
|
||||
'class' => ['remove-button', 'autosubmit'],
|
||||
'label' => new Icon('minus', ['title' => 'Remove item']),
|
||||
'value' => $tempNumberItem,
|
||||
'formnovalidate' => true
|
||||
]
|
||||
);
|
||||
|
||||
$propertyField->registerElement($removeItem);
|
||||
$propertyField->addHtml($removeItem);
|
||||
if (! $removedItems && $removeItem->hasBeenPressed()) {
|
||||
$field->remove($propertyField);
|
||||
$removedItemIdx = (int) $removeItem->getValue();
|
||||
$removedItems += 1;
|
||||
} else {
|
||||
$propertyField->populate($field->getPopulatedValue('property-' . $numberItem) ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
$addedItemsCount -= $removedItems;
|
||||
$addedCountElement->setValue($addedItemsCount);
|
||||
|
||||
if ($addedItemsCount === 0 && $initialItemsCount === 0 && ! empty($inheritedValue)) {
|
||||
$field->addElement(
|
||||
'textarea',
|
||||
'inherited-value',
|
||||
[
|
||||
'label' => sprintf($this->translate('Inherited from "%s"'), $inheritedValue[1]),
|
||||
'value' => json_encode($inheritedValue[0], JSON_PRETTY_PRINT),
|
||||
'readonly' => true,
|
||||
'rows' => 10
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$field->addElement($addItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function fetchPropertyItems(UuidInterface $parentUuid): ?array
|
||||
{
|
||||
$db = $this->db->getDbAdapter();
|
||||
$query = $db->select()
|
||||
->from('director_property')
|
||||
->where('parent_uuid = ?', $parentUuid->getBytes());
|
||||
|
||||
return $db->fetchAll($query, fetchMode: PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
protected function fetchFieldType(string $propertyType, bool $instantiable = false): string
|
||||
{
|
||||
// works only in PHP 8.0 and greater
|
||||
return match ($propertyType) {
|
||||
'bool' => 'boolean',
|
||||
'array' => $instantiable
|
||||
? 'extensibleSet'
|
||||
: 'collection',
|
||||
'dict' => 'collection',
|
||||
default => 'text',
|
||||
};
|
||||
}
|
||||
|
||||
private function isPropertyInstantiable(string $name): string
|
||||
{
|
||||
return isset($this->objectProperties[$name]) ? $this->objectProperties[$name]['instantiable'] : 'n';
|
||||
}
|
||||
|
||||
public function getValues()
|
||||
{
|
||||
$values = [];
|
||||
foreach ($this->getElements() as $element) {
|
||||
if (! $element->isIgnored()) {
|
||||
$value = $element->getValue();
|
||||
if ($element instanceof FieldsetElement) {
|
||||
foreach ($value as $key => $item) {
|
||||
if (in_array($key, ['initial-count', 'added-count', 'prefixes', 'inherited-value'], true)) {
|
||||
unset($value[$key]);
|
||||
} elseif (substr($key, 0, strlen('item')) === 'item') {
|
||||
$idx = (int) substr($key, -1);
|
||||
$value[$idx] = $value[$key];
|
||||
unset($value[$key]);
|
||||
} elseif (substr($key, 0, strlen('property')) === 'property') {
|
||||
$label = $value[$key]['label'] ?? '';
|
||||
unset($value[$key]['label']);
|
||||
|
||||
$value[$label] = $value[$key];
|
||||
unset($value[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$values[$element->getName()] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
public function populate($values)
|
||||
{
|
||||
foreach ($values as $name => $value) {
|
||||
$newValues = [];
|
||||
if (is_array($value)) {
|
||||
if (
|
||||
$this->isPropertyInstantiable($name) === 'y'
|
||||
&& array_keys($value) === range(0, count($value) - 1)
|
||||
) {
|
||||
$values[$name] = implode(',', $value);
|
||||
} elseif ($this->isPropertyInstantiable($name) === 'y') {
|
||||
$nestedValues = [];
|
||||
$i = 0;
|
||||
$prefixes = [];
|
||||
foreach ($value as $key => $item) {
|
||||
if (! is_array($item)) {
|
||||
break;
|
||||
} else {
|
||||
$nestedValues["property-$i"] = array_merge(
|
||||
['label' => $key],
|
||||
$item,
|
||||
);
|
||||
|
||||
$prefixes[] = $i;
|
||||
$i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($nestedValues)) {
|
||||
$nestedValues['initial-count'] = $i;
|
||||
$nestedValues['prefixes'] = implode(',', $prefixes);
|
||||
unset($values[$name]);
|
||||
|
||||
$values[$name] = $nestedValues;
|
||||
} elseif (array_keys($value) === range(0, count($value) - 1)) {
|
||||
$newValue = [];
|
||||
foreach (array_values($value) as $idx => $item) {
|
||||
$newValue["item-$idx"] = $item;
|
||||
}
|
||||
|
||||
$values[$name] = $newValue;
|
||||
}
|
||||
} elseif (array_keys($value) === range(0, count($value) - 1)) {
|
||||
$newValue = [];
|
||||
foreach (array_values($value) as $idx => $item) {
|
||||
$newValue["item-$idx"] = $item;
|
||||
}
|
||||
|
||||
$values[$name] = $newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parent::populate($values);
|
||||
}
|
||||
|
||||
private function filterEmpty(array $array): array
|
||||
{
|
||||
return array_filter(
|
||||
array_map(function ($item) {
|
||||
if (! is_array($item)) {
|
||||
// Recursively clean nested arrays
|
||||
return $item;
|
||||
}
|
||||
|
||||
return $this->filterEmpty($item);
|
||||
}, $array),
|
||||
function ($item) {
|
||||
return is_bool($item) || ! empty($item);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected function onSuccess()
|
||||
{
|
||||
$vars = $this->object->vars();
|
||||
|
||||
$modified = false;
|
||||
var_dump($this->getValues());die;
|
||||
foreach ($this->getValues() as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$value = $this->filterEmpty($value);
|
||||
}
|
||||
|
||||
if (! is_bool($value) && empty($value)) {
|
||||
$vars->set($key, null);
|
||||
} else {
|
||||
$vars->set($key, $value);
|
||||
}
|
||||
|
||||
if ($modified === false && $vars->hasBeenModified()) {
|
||||
$modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
$vars->storeToDb($this->object);
|
||||
|
||||
if ($modified) {
|
||||
Notification::success(
|
||||
sprintf(
|
||||
$this->translate('Custom variables have been successfully modified for %s'),
|
||||
$this->object->getObjectName(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Notification::success($this->translate('There is nothing to change.'));
|
||||
}
|
||||
}
|
||||
}
|
172
application/forms/ObjectPropertyForm.php
Normal file
172
application/forms/ObjectPropertyForm.php
Normal file
@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace Icinga\Module\Director\Forms;
|
||||
|
||||
use Icinga\Module\Director\Data\Db\DbConnection;
|
||||
use Icinga\Module\Director\Objects\IcingaHost;
|
||||
use Icinga\Module\Director\Objects\IcingaObject;
|
||||
use Icinga\Module\Director\Web\Form\Decorator\ExtensibleSetDecorator;
|
||||
use Icinga\Web\Session;
|
||||
use ipl\I18n\Translation;
|
||||
use ipl\Validator\CallbackValidator;
|
||||
use ipl\Web\Common\CsrfCounterMeasure;
|
||||
use ipl\Web\Compat\CompatForm;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
class ObjectPropertyForm extends CompatForm
|
||||
{
|
||||
use CsrfCounterMeasure;
|
||||
use Translation;
|
||||
|
||||
protected $properties = [];
|
||||
|
||||
protected $objectProperties = [];
|
||||
|
||||
public function __construct(
|
||||
public readonly DbConnection $db,
|
||||
public readonly IcingaObject $object,
|
||||
protected bool $isRemoval = false,
|
||||
protected ?UuidInterface $propertyUuid = null
|
||||
) {
|
||||
$this->properties = $this->getProperties();
|
||||
$this->objectProperties = $this->getProperties($this->object->uuid);
|
||||
}
|
||||
|
||||
public function getPropertyName(): string
|
||||
{
|
||||
$propertyUuid = $this->getValue('property');
|
||||
if ($propertyUuid) {
|
||||
return $this->properties[$propertyUuid] ?? '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function assemble(): void
|
||||
{
|
||||
$this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId()));
|
||||
$propertyElement = $this->createElement(
|
||||
'select',
|
||||
'property',
|
||||
[
|
||||
'label' => $this->translate('Property'),
|
||||
'required' => true,
|
||||
'class' => ['autosubmit'],
|
||||
'disabledOptions' => [''],
|
||||
'value' => '',
|
||||
'options' => array_merge(
|
||||
['' => $this->translate('Please choose a property')],
|
||||
$this->isRemoval
|
||||
? $this->getProperties($this->object->uuid)
|
||||
: $this->getProperties()
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
$this->addElement($propertyElement);
|
||||
|
||||
if (! $this->isRemoval) {
|
||||
$propertyElement->addAttributes(
|
||||
[
|
||||
'validators' => [new CallbackValidator(function ($value, $validator) {
|
||||
if (array_key_exists($value, $this->objectProperties)) {
|
||||
$validator->addMessage($this->translate('Property already exists'));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})]
|
||||
]
|
||||
);
|
||||
|
||||
$this->addElement(
|
||||
'select',
|
||||
'mandatory',
|
||||
[
|
||||
'label' => $this->translate('Mandatory'),
|
||||
'required' => true,
|
||||
'value' => 'n',
|
||||
'options' => ['y' => 'Yes', 'n' => 'No']
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$this->addElement('submit', 'submit', [
|
||||
'label' => $this->isRemoval
|
||||
? $this->translate('Remove')
|
||||
: $this->translate('Add')
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getProperties(?string $objectUuid = null): array
|
||||
{
|
||||
$query = $this->db->getDbAdapter()
|
||||
->select()
|
||||
->from(['dp' => 'director_property'], ['uuid' => 'dp.uuid', 'key_name' => 'dp.key_name'])
|
||||
->where('parent_uuid IS NULL');
|
||||
|
||||
if ($objectUuid) {
|
||||
$query->join(['iop' => 'icinga_host_property'], 'iop.property_uuid = dp.uuid')
|
||||
->where('iop.host_uuid = ?', $objectUuid);
|
||||
}
|
||||
|
||||
$properties = $this->db->getDbAdapter()->fetchAll($query);
|
||||
|
||||
$propUuidKeyPairs = [];
|
||||
foreach ($properties as $property) {
|
||||
$propUuidKeyPairs[Uuid::fromBytes($property->uuid)->toString()] = $property->key_name;
|
||||
}
|
||||
|
||||
return $propUuidKeyPairs;
|
||||
}
|
||||
|
||||
protected function onSuccess()
|
||||
{
|
||||
$formProperty = $this->getValue('property');
|
||||
if ($this->isRemoval) {
|
||||
$varName = $this->getPropertyName();
|
||||
$propertyAsHostVar = $this->db->getDbAdapter()->fetchAll(
|
||||
$this->db
|
||||
->getDbAdapter()
|
||||
->select()
|
||||
->from('icinga_host_var')
|
||||
->where('varname = ?', $varName)
|
||||
);
|
||||
|
||||
$objectId = (int) $this->object->get('id');
|
||||
$objectsToCleanUp = [$objectId];
|
||||
foreach ($propertyAsHostVar as $propertyAsHostVarRow) {
|
||||
$host = IcingaHost::loadWithAutoIncId($propertyAsHostVarRow->host_id, $this->db);
|
||||
|
||||
if (in_array($objectId, $host->listAncestorIds(), true)) {
|
||||
$objectsToCleanUp[] = (int) $host->get('id');
|
||||
}
|
||||
}
|
||||
|
||||
$where = $this->db->getDbAdapter()->quoteInto('varname = ?', $varName);
|
||||
$where .= $this->db->getDbAdapter()->quoteInto(' AND host_id IN (?)', $objectsToCleanUp);
|
||||
$this->db->getDbAdapter()
|
||||
->delete('icinga_host_var', $where);
|
||||
|
||||
$this->db->delete(
|
||||
'icinga_host_property',
|
||||
Filter::matchAll(
|
||||
Filter::where('host_uuid', $this->object->uuid),
|
||||
Filter::where('property_uuid', Uuid::fromString($formProperty)->getBytes())
|
||||
)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->insert(
|
||||
'icinga_host_property',
|
||||
[
|
||||
'host_uuid' => $this->object->uuid,
|
||||
'property_uuid' => Uuid::fromString($this->getValue('property'))->getBytes()
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
276
application/forms/PropertyForm.php
Normal file
276
application/forms/PropertyForm.php
Normal file
@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
namespace Icinga\Module\Director\Forms;
|
||||
|
||||
use Icinga\Data\Filter\Filter;
|
||||
use Icinga\Module\Director\Data\Db\DbConnection;
|
||||
use Icinga\Web\Session;
|
||||
use ipl\Html\Contract\FormSubmitElement;
|
||||
use ipl\I18n\Translation;
|
||||
use ipl\Web\Common\CsrfCounterMeasure;
|
||||
use ipl\Web\Compat\CompatForm;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
class PropertyForm extends CompatForm
|
||||
{
|
||||
use CsrfCounterMeasure;
|
||||
use Translation;
|
||||
|
||||
/** @var bool */
|
||||
private $hideKeyNameElement = false;
|
||||
|
||||
private $isNestedField = false;
|
||||
|
||||
public function __construct(
|
||||
protected DbConnection $db,
|
||||
protected ?UuidInterface $uuid = null,
|
||||
protected bool $field = false,
|
||||
protected ?UuidInterface $parentUuid = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function getUUid(): ?UuidInterface
|
||||
{
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
public function getParentUUid(): ?UuidInterface
|
||||
{
|
||||
return $this->parentUuid;
|
||||
}
|
||||
|
||||
public function setHideKeyNameElement(bool $hideKeyNameElement): self
|
||||
{
|
||||
$this->hideKeyNameElement = $hideKeyNameElement;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setIsNestedField(bool $isNestedField): self
|
||||
{
|
||||
$this->isNestedField = $isNestedField;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function assemble(): void
|
||||
{
|
||||
$this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId()));
|
||||
|
||||
if ($this->hideKeyNameElement) {
|
||||
$db = $this->db->getDbAdapter();
|
||||
|
||||
$query = $db->select()
|
||||
->from('director_property', ['count' => 'COUNT(*)'])
|
||||
->where('parent_uuid = ?', $this->parentUuid->getBytes());
|
||||
$this->addElement(
|
||||
'hidden',
|
||||
'key_name',
|
||||
[
|
||||
'label' => $this->translate('Key'),
|
||||
'required' => true,
|
||||
'value' => $db->fetchOne($query)
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$this->addElement(
|
||||
'text',
|
||||
'key_name',
|
||||
[
|
||||
'label' => $this->translate('Key'),
|
||||
'required' => true
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$this->addElement(
|
||||
'text',
|
||||
'label',
|
||||
[
|
||||
'label' => $this->translate('Label'),
|
||||
'required' => true
|
||||
]
|
||||
);
|
||||
|
||||
$types = [
|
||||
'string' => 'String',
|
||||
'number' => 'Number',
|
||||
'bool' => 'Boolean',
|
||||
];
|
||||
|
||||
if (! $this->isNestedField) {
|
||||
$types += ['array' => 'Array', 'dict' => 'Dictionary'];
|
||||
}
|
||||
|
||||
$this->addElement(
|
||||
'select',
|
||||
'value_type',
|
||||
[
|
||||
'label' => $this->translate('Type'),
|
||||
'class' => 'autosubmit',
|
||||
'required' => true,
|
||||
'disabledOptions' => [''],
|
||||
'value' => 'string',
|
||||
'options' => $types
|
||||
]
|
||||
);
|
||||
|
||||
$type = $this->getValue('value_type');
|
||||
if ($type === 'dict' || $type === 'array') {
|
||||
$instantiableElement = $this->createElement(
|
||||
'checkbox',
|
||||
'instantiable',
|
||||
[
|
||||
'label' => $this->translate('Instantiable by users'),
|
||||
'class' => 'autosubmit',
|
||||
'checkedValue' => 'y',
|
||||
'uncheckedValue' => 'n',
|
||||
'value' => 'n'
|
||||
]
|
||||
);
|
||||
|
||||
if ($type === 'dict') {
|
||||
$instantiableElement->getAttributes()->add('disabled', $this->parentUuid !== null);
|
||||
}
|
||||
|
||||
$this->addElement($instantiableElement);
|
||||
|
||||
|
||||
if ($type === 'array' && $this->getValue('instantiable') === 'y') {
|
||||
$this->addElement(
|
||||
'select',
|
||||
'item_type',
|
||||
[
|
||||
'label' => $this->translate('Item Type'),
|
||||
'class' => 'autosubmit',
|
||||
'disabledOptions' => [''],
|
||||
'value' => 'string',
|
||||
'options' => array_slice($types, 0, 2)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->addElement('submit', 'submit', [
|
||||
'label' => $this->uuid ? $this->translate('Save') : $this->translate('Add')
|
||||
]);
|
||||
|
||||
if ($this->uuid) {
|
||||
/** @var FormSubmitElement $deleteButton */
|
||||
$deleteButton = $this->createElement(
|
||||
'submit',
|
||||
'delete',
|
||||
[
|
||||
'label' => $this->translate('Delete'),
|
||||
'class' => 'btn-remove',
|
||||
'formnovalidate' => true
|
||||
]
|
||||
);
|
||||
|
||||
$this->registerElement($deleteButton);
|
||||
$this->getElement('submit')
|
||||
->getWrapper()
|
||||
->prepend($deleteButton);
|
||||
}
|
||||
}
|
||||
|
||||
public function hasBeenSubmitted()
|
||||
{
|
||||
if ($this->getPressedSubmitElement() !== null && $this->getPressedSubmitElement()->getName() === 'delete') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return parent::hasBeenSubmitted();
|
||||
}
|
||||
|
||||
public function isValid()
|
||||
{
|
||||
if ($this->getPressedSubmitElement()->getName() === 'delete') {
|
||||
$csrfElement = $this->getElement('CSRFToken');
|
||||
|
||||
return $csrfElement->isValid();
|
||||
}
|
||||
|
||||
return parent::isValid();
|
||||
}
|
||||
|
||||
protected function onSuccess()
|
||||
{
|
||||
if ($this->getPressedSubmitElement()->getName() === 'delete') {
|
||||
$this->db->delete('director_property', Filter::where('parent_uuid', $this->uuid->getBytes()));
|
||||
$this->db->delete('director_property', Filter::where('uuid', $this->uuid->getBytes()));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$values = $this->getValues();
|
||||
if ($this->uuid === null) {
|
||||
$this->uuid = Uuid::uuid4();
|
||||
if ($this->field) {
|
||||
$values = array_merge(
|
||||
[
|
||||
'uuid' => $this->uuid->getBytes(),
|
||||
'parent_uuid' => $this->parentUuid->getBytes()
|
||||
],
|
||||
$values
|
||||
);
|
||||
} else {
|
||||
$values = array_merge(
|
||||
['uuid' => $this->uuid->getBytes()],
|
||||
$values
|
||||
);
|
||||
}
|
||||
|
||||
$instantiatedEntry = [];
|
||||
if (isset($values['item_type'])) {
|
||||
$instantiatedEntry = [
|
||||
'uuid' => Uuid::uuid4()->getBytes(),
|
||||
'key_name' => '0',
|
||||
'value_type' => $values['item_type'],
|
||||
'parent_uuid' => $this->uuid->getBytes(),
|
||||
'instantiable' => 'n',
|
||||
];
|
||||
|
||||
unset($values['item_type']);
|
||||
}
|
||||
|
||||
$this->db->insert('director_property', $values);
|
||||
|
||||
if (! empty($instantiatedEntry)) {
|
||||
$this->db->insert('director_property', $instantiatedEntry);
|
||||
}
|
||||
} else {
|
||||
$instantiatedEntry = [];
|
||||
if (isset($values['item_type']) && $values['instantiable'] === 'y') {
|
||||
$instantiatedEntry = [
|
||||
'uuid' => Uuid::uuid4()->getBytes(),
|
||||
'key_name' => '0',
|
||||
'value_type' => $values['item_type'],
|
||||
'parent_uuid' => $this->uuid->getBytes(),
|
||||
'instantiable' => 'n',
|
||||
];
|
||||
|
||||
unset($values['item_type']);
|
||||
}
|
||||
|
||||
$this->db->update(
|
||||
'director_property',
|
||||
$values,
|
||||
Filter::where('uuid', $this->uuid->getBytes())
|
||||
);
|
||||
|
||||
$this->db->delete(
|
||||
'director_property',
|
||||
Filter::matchAll(
|
||||
Filter::where('parent_uuid', $this->uuid->getBytes()),
|
||||
Filter::where('key_name', '0')
|
||||
)
|
||||
);
|
||||
|
||||
if (! empty($instantiatedEntry)) {
|
||||
$this->db->insert('director_property', $instantiatedEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -175,3 +175,9 @@ $section->add(N_('Deployments'))
|
||||
->setUrl('director/config/deployments')
|
||||
->setPriority(902)
|
||||
->setPermission(Permission::DEPLOYMENTS);
|
||||
$section->add(N_('Properties'))
|
||||
->setUrl('director/properties')
|
||||
->setPriority(903);
|
||||
|
||||
$this->provideCssFile('property-table.less');
|
||||
$this->provideCssFile('custom-properties-form.less');
|
||||
|
@ -10,7 +10,6 @@ use Icinga\Exception\ProgrammingError;
|
||||
use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
|
||||
use Icinga\Module\Director\Db\Branch\Branch;
|
||||
use Icinga\Module\Director\Db\Branch\BranchedObject;
|
||||
use Icinga\Module\Director\Db\Branch\BranchSupport;
|
||||
use Icinga\Module\Director\Db\Branch\UuidLookup;
|
||||
use Icinga\Module\Director\Deployment\DeploymentInfo;
|
||||
use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
|
||||
@ -18,6 +17,7 @@ use Icinga\Module\Director\Exception\NestingError;
|
||||
use Icinga\Module\Director\Forms\DeploymentLinkForm;
|
||||
use Icinga\Module\Director\Forms\IcingaCloneObjectForm;
|
||||
use Icinga\Module\Director\Forms\IcingaObjectFieldForm;
|
||||
use Icinga\Module\Director\Forms\ObjectPropertyForm;
|
||||
use Icinga\Module\Director\Objects\IcingaCommand;
|
||||
use Icinga\Module\Director\Objects\IcingaObject;
|
||||
use Icinga\Module\Director\Objects\IcingaObjectGroup;
|
||||
@ -34,7 +34,10 @@ use Icinga\Module\Director\Web\Table\IcingaObjectDatafieldTable;
|
||||
use Icinga\Module\Director\Web\Tabs\ObjectTabs;
|
||||
use Icinga\Module\Director\Web\Widget\BranchedObjectHint;
|
||||
use gipfl\IcingaWeb2\Link;
|
||||
use Icinga\Module\Director\Web\Widget\ObjectPropertyTable;
|
||||
use Icinga\Web\Notification;
|
||||
use ipl\Html\Html;
|
||||
use ipl\Web\Url;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
@ -101,6 +104,10 @@ abstract class ObjectController extends ActionController
|
||||
|
||||
protected function initializeWebRequest()
|
||||
{
|
||||
if ($this->getRequest()->getActionName() === 'add-property') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->getRequest()->getActionName() === 'add') {
|
||||
$this->addSingleTab(
|
||||
sprintf($this->translate('Add %s'), ucfirst($this->getType())),
|
||||
@ -268,6 +275,76 @@ abstract class ObjectController extends ActionController
|
||||
}
|
||||
}
|
||||
|
||||
public function addPropertyAction()
|
||||
{
|
||||
$this->assertPermission('director/admin');
|
||||
$object = $this->requireObject();
|
||||
$this->view->title = sprintf($this->translate('Add Custom Property: %s'), $this->object->getObjectName());
|
||||
try {
|
||||
$this->fetchPropertyForm($object);
|
||||
} catch (NestingError $e) {
|
||||
$this->content()->add(Hint::error($e->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
public function removePropertyAction()
|
||||
{
|
||||
$this->assertPermission('director/admin');
|
||||
$object = $this->requireObject();
|
||||
$this->view->title = sprintf($this->translate('Remove Custom Property: %s'), $this->object->getObjectName());
|
||||
try {
|
||||
$this->fetchPropertyForm($object, true);
|
||||
} catch (NestingError $e) {
|
||||
$this->content()->add(Hint::error($e->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
protected function fetchPropertyForm(IcingaObject $object, bool $isRemoval = false)
|
||||
{
|
||||
$propertyUuid = $this->params->get('property_uuid');
|
||||
$objectUuid = $this->object->get('uuid');
|
||||
$objectType = $this->object->getShortTableName();
|
||||
$formData = [];
|
||||
|
||||
if ($propertyUuid) {
|
||||
$propertyUuid = Uuid::fromString($propertyUuid);
|
||||
$objectPropertyQuery = $this->db()
|
||||
->select()
|
||||
->from('icinga_host_property', ['required'])
|
||||
->where($objectType . '_uuid ', $objectUuid)
|
||||
->where('property_uuid', $propertyUuid);
|
||||
|
||||
$formData = [
|
||||
'property' => $propertyUuid->toString(),
|
||||
'required' => $this->db()->fetchOne($objectPropertyQuery)
|
||||
];
|
||||
}
|
||||
|
||||
$form = (new ObjectPropertyForm($this->db(), $object, $isRemoval, $propertyUuid))
|
||||
->populate($formData)
|
||||
->setAction(Url::fromRequest()->getAbsoluteUrl())
|
||||
->on(ObjectPropertyForm::ON_SUCCESS, function (ObjectPropertyForm $form) use ($objectUuid, $isRemoval) {
|
||||
if ($isRemoval) {
|
||||
Notification::success(sprintf(
|
||||
$this->translate('Property %s has successfully been deleted'),
|
||||
$form->getPropertyName()
|
||||
));
|
||||
} else {
|
||||
Notification::success(sprintf(
|
||||
sprintf($this->translate('Property%s has successfully been added'), $form->getPropertyName())
|
||||
));
|
||||
}
|
||||
|
||||
$this->redirectNow(Url::fromPath(
|
||||
'director/' . $this->getType() . '/variables',
|
||||
['uuid' => UUid::fromBytes($objectUuid)->toString()]
|
||||
));
|
||||
})
|
||||
->handleRequest($this->getServerRequest());
|
||||
|
||||
$this->content()->add($form);
|
||||
}
|
||||
|
||||
protected function addFieldsFormAndTable($object, $type)
|
||||
{
|
||||
$form = IcingaObjectFieldForm::load()
|
||||
|
@ -15,6 +15,7 @@ use Icinga\Module\Director\Exception\NestingError;
|
||||
use Icinga\Module\Director\Hook\IcingaObjectFormHook;
|
||||
use Icinga\Module\Director\IcingaConfig\StateFilterSet;
|
||||
use Icinga\Module\Director\IcingaConfig\TypeFilterSet;
|
||||
use Icinga\Module\Director\Objects\IcingaHost;
|
||||
use Icinga\Module\Director\Objects\IcingaTemplateChoice;
|
||||
use Icinga\Module\Director\Objects\IcingaCommand;
|
||||
use Icinga\Module\Director\Objects\IcingaObject;
|
||||
@ -820,14 +821,24 @@ abstract class DirectorObjectForm extends DirectorForm
|
||||
if ($this->object !== null) {
|
||||
$this->setDefaultsFromObject($this->object);
|
||||
}
|
||||
$this->prepareFields($this->object());
|
||||
|
||||
$isHost = $this->object instanceof IcingaHost;
|
||||
|
||||
if (! $isHost) {
|
||||
$this->prepareFields($this->object());
|
||||
}
|
||||
|
||||
IcingaObjectFormHook::callOnSetup($this);
|
||||
if ($this->hasBeenSent()) {
|
||||
$this->handlePost();
|
||||
}
|
||||
|
||||
try {
|
||||
$this->loadInheritedProperties();
|
||||
$this->addFields();
|
||||
if (! $isHost) {
|
||||
$this->addFields();
|
||||
}
|
||||
|
||||
$this->callOnRequestCallables();
|
||||
} catch (Exception $e) {
|
||||
$this->addUniqueException($e);
|
||||
@ -848,7 +859,7 @@ abstract class DirectorObjectForm extends DirectorForm
|
||||
$this->populate($post);
|
||||
$values = $this->getValues();
|
||||
|
||||
if ($object instanceof IcingaObject) {
|
||||
if ($object instanceof IcingaObject && $object->getShortTableName() !== 'host') {
|
||||
$this->setCustomVarValues($post);
|
||||
}
|
||||
|
||||
|
71
library/Director/Web/Form/Element/ArrayElement.php
Normal file
71
library/Director/Web/Form/Element/ArrayElement.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Icinga\Module\Director\Web\Form\Element;
|
||||
|
||||
use ipl\Html\Attribute;
|
||||
use ipl\Web\FormElement\TermInput;
|
||||
use ipl\Web\FormElement\TermInput\RegisteredTerm;
|
||||
|
||||
class ArrayElement extends TermInput
|
||||
{
|
||||
/** @var string */
|
||||
private $placeHolder = '';
|
||||
|
||||
protected $defaultAttributes = ['class' => 'array-input'];
|
||||
|
||||
public function setPlaceHolder(string $placeHolder): static
|
||||
{
|
||||
$this->placeHolder = $placeHolder;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function assemble()
|
||||
{
|
||||
parent::assemble();
|
||||
|
||||
$valuePlaceHolder = $this->translate('Separate multiple values by comma.');
|
||||
if ($this->placeHolder) {
|
||||
$valuePlaceHolder = $this->placeHolder . '. ' . $valuePlaceHolder;
|
||||
}
|
||||
|
||||
$this->getElement('value')
|
||||
->getAttributes()
|
||||
->registerAttributeCallback('placeholder', function () use ($valuePlaceHolder) {
|
||||
return $valuePlaceHolder;
|
||||
});
|
||||
}
|
||||
|
||||
public function getValue($name = null, $default = null)
|
||||
{
|
||||
if ($name !== null) {
|
||||
return parent::getValue($name, $default);
|
||||
}
|
||||
|
||||
$terms = [];
|
||||
foreach ($this->getTerms() as $term) {
|
||||
$terms[] = $term->render(',');
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
public function setValue($value)
|
||||
{
|
||||
if (is_array($value) && isset($value['value'])) {
|
||||
$separatedTerms = $value['value'] ?? '';
|
||||
parent::setValue($value);
|
||||
} elseif (is_array($value)) {
|
||||
$separatedTerms = implode(',', $value);
|
||||
} else {
|
||||
$separatedTerms = $value;
|
||||
}
|
||||
|
||||
$terms = [];
|
||||
foreach ($this->parseValue((string) $separatedTerms) as $term) {
|
||||
$terms[] = new RegisteredTerm($term);
|
||||
}
|
||||
|
||||
return $this->setTerms(...$terms);
|
||||
}
|
||||
}
|
64
library/Director/Web/Form/Element/IplBoolean.php
Normal file
64
library/Director/Web/Form/Element/IplBoolean.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Icinga\Module\Director\Web\Form\Element;
|
||||
|
||||
use ipl\Html\FormElement\SelectElement;
|
||||
use ipl\I18n\Translation;
|
||||
|
||||
class IplBoolean extends SelectElement
|
||||
{
|
||||
use Translation;
|
||||
|
||||
public function __construct($name, $attributes = null)
|
||||
{
|
||||
parent::__construct($name, $attributes);
|
||||
|
||||
$options = [
|
||||
'y' => $this->translate('Yes'),
|
||||
'n' => $this->translate('No'),
|
||||
];
|
||||
if (! $this->isRequired()) {
|
||||
$options = [
|
||||
null => $this->translate('- Please choose -'),
|
||||
] + $options;
|
||||
}
|
||||
|
||||
$this->setOptions($options);
|
||||
}
|
||||
|
||||
public function setValue($value)
|
||||
{
|
||||
if ($value === 'y' || $value === true) {
|
||||
return parent::setValue('y');
|
||||
} elseif ($value === 'n' || $value === false) {
|
||||
return parent::setValue('n');
|
||||
}
|
||||
|
||||
// Hint: this will fail
|
||||
return parent::setValue($value);
|
||||
}
|
||||
|
||||
public function getValue()
|
||||
{
|
||||
if ($this->value === 'y') {
|
||||
return true;
|
||||
} elseif ($this->value === 'n') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
protected function isSelectedOption($optionValue): bool
|
||||
{
|
||||
$optionValue = match ($optionValue) {
|
||||
'y' => true,
|
||||
'n' => false,
|
||||
default => null
|
||||
};
|
||||
|
||||
return parent::isSelectedOption(
|
||||
$optionValue
|
||||
);
|
||||
}
|
||||
}
|
@ -93,7 +93,15 @@ class ObjectTabs extends Tabs
|
||||
));
|
||||
}
|
||||
|
||||
if ($auth->hasPermission(Permission::ADMIN) && $this->hasFields()) {
|
||||
if ($this->object->getShortTableName() === 'host') {
|
||||
if ($auth->hasPermission(Permission::ADMIN)) {
|
||||
$this->add('variables', array(
|
||||
'url' => sprintf('director/%s/variables', $type),
|
||||
'urlParams' => $params,
|
||||
'label' => $this->translate('Custom Variables')
|
||||
));
|
||||
}
|
||||
} elseif ($auth->hasPermission(Permission::ADMIN) && $this->hasFields()) {
|
||||
$this->add('fields', array(
|
||||
'url' => sprintf('director/%s/fields', $type),
|
||||
'urlParams' => $params,
|
||||
|
57
library/Director/Web/Widget/ObjectPropertyTable.php
Normal file
57
library/Director/Web/Widget/ObjectPropertyTable.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Icinga\Module\Director\Web\Widget;
|
||||
|
||||
use ipl\Html\Html;
|
||||
use ipl\Html\HtmlElement;
|
||||
use ipl\Html\Table;
|
||||
use ipl\I18n\Translation;
|
||||
use ipl\Web\Url;
|
||||
use ipl\Web\Widget\Link;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
class ObjectPropertyTable extends Table
|
||||
{
|
||||
use Translation;
|
||||
|
||||
protected $defaultAttributes = [
|
||||
'class' => 'common-table table-row-selectable object-property-table'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected UuidInterface $objectUuid,
|
||||
protected array $properties
|
||||
) {
|
||||
}
|
||||
|
||||
protected function assemble()
|
||||
{
|
||||
$this->add(static::tr([
|
||||
static::th([HtmlElement::create('p', null, $this->translate('Key'))])->setSeparator(' '),
|
||||
static::th([HtmlElement::create('p', null, $this->translate('Label'))])->setSeparator(' '),
|
||||
static::th([HtmlElement::create('p', null, $this->translate('Type'))]),
|
||||
static::th([HtmlElement::create('p', null, $this->translate('Mandatory'))])
|
||||
]));
|
||||
foreach ($this->properties as $property) {
|
||||
$objectPropertyLink = new Link(
|
||||
$property->key_name,
|
||||
Url::fromPath(
|
||||
'director/host/properties',
|
||||
[
|
||||
'uuid' => $this->objectUuid->toString(),
|
||||
'property_uuid' => Uuid::fromBytes($property->uuid)->toString()
|
||||
]
|
||||
),
|
||||
['target' => '_blank']
|
||||
);
|
||||
|
||||
$this->add(static::tr([
|
||||
static::td([HtmlElement::create('p', null, $objectPropertyLink)])->setSeparator(' '),
|
||||
static::td([HtmlElement::create('p', null, $property->label)])->setSeparator(' '),
|
||||
static::td([HtmlElement::create('p', null, $property->value_type)])->setSeparator(' '),
|
||||
static::td([HtmlElement::create('p', null, $property->required)])
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
50
library/Director/Web/Widget/PropertyTable.php
Normal file
50
library/Director/Web/Widget/PropertyTable.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Icinga\Module\Director\Web\Widget;
|
||||
|
||||
use ipl\Html\HtmlElement;
|
||||
use ipl\Html\Table;
|
||||
use ipl\Web\Url;
|
||||
use ipl\Web\Widget\Link;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class PropertyTable extends Table
|
||||
{
|
||||
protected $defaultAttributes = [
|
||||
'class' => 'common-table table-row-selectable property-table',
|
||||
'data-base-target' => '_next',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected array $properties,
|
||||
protected bool $isFieldsTable = false
|
||||
) {
|
||||
}
|
||||
|
||||
protected function assemble()
|
||||
{
|
||||
foreach ($this->properties as $property) {
|
||||
if ($this->isFieldsTable) {
|
||||
$url = Url::fromPath(
|
||||
'director/property/edit-field',
|
||||
[
|
||||
'uuid' => Uuid::fromBytes($property->uuid)->toString(),
|
||||
'parent_uuid' => Uuid::fromBytes($property->parent_uuid)->toString()
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$url = Url::fromPath(
|
||||
'director/property',
|
||||
['uuid' => Uuid::fromBytes($property->uuid)->toString()]
|
||||
);
|
||||
}
|
||||
|
||||
$this->add(static::tr([
|
||||
static::td([HtmlElement::create('strong', null, new Link($property->key_name, $url))])
|
||||
->setSeparator(' '),
|
||||
static::td([HtmlElement::create('p', null, $property->label)])->setSeparator(' '),
|
||||
static::td([HtmlElement::create('p', null, $property->value_type)])
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
29
public/css/custom-properties-form.less
Normal file
29
public/css/custom-properties-form.less
Normal file
@ -0,0 +1,29 @@
|
||||
.custom-properties-form {
|
||||
.control-group:has(> fieldset) {
|
||||
position: relative;
|
||||
padding-right: 2em;
|
||||
|
||||
fieldset:not(.array-input) {
|
||||
border: 1px solid @gray-light;
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
&.dictionary-item legend {
|
||||
background-color: @icinga-blue-light;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 2em;
|
||||
width: 2em;
|
||||
border: none;
|
||||
justify-content: center;
|
||||
color: @color-critical;
|
||||
&:not(:hover) {
|
||||
background-color: @low-sat-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -490,6 +490,10 @@ form.director-form .host-group-links {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
details {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// TODO: figure out whether form.editor and filter-related CSS is still required
|
||||
div.filter > form.search, div.filter > a {
|
||||
// Duplicated by quicksearch
|
||||
|
3
public/css/property-table.less
Normal file
3
public/css/property-table.less
Normal file
@ -0,0 +1,3 @@
|
||||
.common-table.property-table {
|
||||
max-width: 100%;
|
||||
}
|
@ -20,6 +20,7 @@
|
||||
this.module.on('rendered', this.rendered);
|
||||
this.module.on('beforerender', this.beforeRender);
|
||||
this.module.on('click', 'fieldset > legend', this.toggleFieldset);
|
||||
// this.module.on('click', 'fieldset > legend', this.toggleFieldset);
|
||||
// Disabled
|
||||
// this.module.on('click', 'div.controls ul.tabs a', this.detailTabClick);
|
||||
this.module.on('click', 'input.related-action', this.extensibleSetAction);
|
||||
@ -731,7 +732,7 @@
|
||||
url = $container.data('icingaUrl');
|
||||
$actions = $('.main-actions', $('#col1'));
|
||||
}
|
||||
if (! $actions.length) {
|
||||
if ($actions) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2444,6 +2444,33 @@ CREATE TABLE branched_icinga_dependency (
|
||||
ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE director_property (
|
||||
uuid binary(16) NOT NULL,
|
||||
parent_uuid binary(16) NULL DEFAULT NULL,
|
||||
key_name varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
label varchar(255) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
|
||||
value_type enum('string', 'number', 'bool', 'array', 'dict') COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
instantiable enum('y', 'n') NOT NULL DEFAULT 'n',
|
||||
PRIMARY KEY (uuid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
CREATE TABLE icinga_host_property (
|
||||
host_uuid binary(16) NOT NULL,
|
||||
property_uuid binary(16) NOT NULL,
|
||||
required enum('y', 'n') NOT NULL DEFAULT 'n',
|
||||
PRIMARY KEY (host_uuid, property_uuid),
|
||||
CONSTRAINT icinga_host_property_host
|
||||
FOREIGN KEY host(host_uuid)
|
||||
REFERENCES icinga_host (uuid)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE,
|
||||
CONSTRAINT icinga_host_custom_property
|
||||
FOREIGN KEY property(property_uuid)
|
||||
REFERENCES director_property (uuid)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
INSERT INTO director_schema_migration
|
||||
(schema_version, migration_time)
|
||||
VALUES (189, NOW());
|
||||
VALUES (190, NOW());
|
||||
|
Loading…
x
Reference in New Issue
Block a user