icingaweb2-module-director/application/forms/DictionaryElements/NestedDictionary.php
Ravi Srinivasa dc4b9cb80f
feat(ui): Add forms, controllers, and REST API handler for custom variable management
Introduce the full UI layer for creating, editing, and assigning custom
variables (DirectorProperties) to Icinga objects:

- CustomvarController: CRUD for global custom variable definitions
- VariablesController: per-object variable assignment
- HostController: host-specific dictionary member management
- SuggestionsController: datalist suggestions with PostgreSQL support
- CustomVariableForm / CustomVariablesForm / DeleteCustomVariableForm:
  forms for managing variables on objects with multipart update support
- DictionaryElements (Dictionary, DictionaryItem, NestedDictionary,
  NestedDictionaryItem): composable form elements for structured types
- ArrayElement, IplBoolean: new reusable form elements
- DatalistEntryValidator: validates datalist-constrained variable values
- ObjectController: fetchNestedDictionaryKeys, multipart reload handling
- IcingaObjectHandler (REST API): expose and accept structured custom
  variables in PUT/POST requests; support PostgreSQL UUID binaries
- ObjectTabs: add Variables tab to all object types
- CSS / JS: styles for item lists, action lists, custom variable forms,
  and host-service deactivation; JS fix for multipart form reloads
- configuration.php: register new routes and the Custom Variables dashlet
2026-06-01 12:32:15 +02:00

218 lines
6.7 KiB
PHP

<?php
namespace Icinga\Module\Director\Forms\DictionaryElements;
use ipl\Html\FormElement\FieldsetElement;
use ipl\Html\Html;
use ipl\Web\Url;
use ipl\Web\Widget\EmptyStateBar;
use ipl\Web\Widget\Link;
use Ramsey\Uuid\UuidInterface;
/**
* @phpstan-import-type DictionaryDataType from Dictionary
*/
class NestedDictionary extends FieldsetElement
{
protected $defaultAttributes = ['class' => ['nested-dictionary', 'nested-fieldset']];
public const UNDEFINED_KEY = '__undefined__';
/** @var array Nested dictionary items */
protected $nestedItems = [];
/** @var UuidInterface Uuid of the dictionary */
private UuidInterface $uuid;
/** @var array{inherited_from: string, value: array} Inherited value */
protected array $inheritedValue;
public function __construct(
string $name,
array $nestedItems,
array $inheritedValues,
$attributes = null
) {
$this->inheritedValue = $inheritedValues;
$this->nestedItems = $nestedItems;
parent::__construct($name, $attributes);
}
public function setUuid(UuidInterface $uuid)
{
$this->uuid = $uuid;
return $this;
}
protected function assemble(): void
{
$expectedCount = (int) $this->getPopulatedValue('count', 0);
$count = 0;
$newCount = 0;
if (! empty($this->inheritedValue['value'])) {
$inheritedFrom = implode(
', ',
array_map(
fn($item) => '"' . trim($item) . '"',
explode(',', $this->inheritedValue['inherited_from'])
)
);
$this->addElement(
'textarea',
'inherited_value',
[
'label' => sprintf(
$this->translate('Inherited from %s'),
$inheritedFrom
),
'value' => $this->inheritedValue['value'],
'class' => 'inherited-value',
'readonly' => true,
'rows' => 10
]
);
}
while ($count < $expectedCount) {
$remove = $this->createElement(
'submitButton',
'remove_' . $count
);
$this->registerElement($remove);
if ($remove->hasBeenPressed()) {
$removedValue = $this->getPopulatedValue($count);
$clearedId = null;
if (isset($removedValue['id'])) {
$clearedId = $removedValue['id'];
}
$this->clearPopulatedValue($remove->getName());
$this->clearPopulatedValue($count);
// Re-index populated values to ensure proper association with form data
foreach (range($count + 1, $expectedCount) as $i) {
$newPopulatedValue = $this->getPopulatedValue($count);
$newId = $newPopulatedValue['id'] ?? null;
$newPopulatedValue['id'] = $clearedId;
$this->populate([$i - 1 => $this->getPopulatedValue($i) ?? []]);
$clearedId = $newId;
}
} else {
$newCount++;
}
$count++;
}
$addButton = $this->createElement('submitButton', 'add_item', [
'label' => $this->translate('Add Item'),
'class' => ['add-item'],
'formnovalidate' => true
]);
if (empty($this->nestedItems)) {
$addButton->addAttributes([
'disabled' => true,
'title' => $this->translate('No fields have been configured for the dictionary'),
]);
}
$this->registerElement($addButton);
if ($addButton->hasBeenPressed()) {
$remove = $this->createElement('submitButton', 'remove_' . $newCount, ['label' => 'Remove Item']);
$this->registerElement($remove);
$newCount++;
}
for ($i = 0; $i < $newCount; $i++) {
$nestedDictionaryProperty = new NestedDictionaryItem($i, $this->nestedItems);
$nestedDictionaryProperty->setRemoveButton($this->getElement('remove_' . $i));
$this->addElement($nestedDictionaryProperty);
}
if ($newCount === 0) {
if (empty($this->nestedItems)) {
$this->addHtml(new EmptyStateBar(Html::sprintf(
$this->translate(
'No fields configured for this dictionary.'
. ' Add fields to the custom variable definition in %s.'
),
new Link(
$this->translate('Custom Variables'),
Url::fromPath('director/customvar', ['uuid' => $this->uuid->toString()]),
['data-base-target' => '_next']
)
)));
} else {
$this->addHtml(new EmptyStateBar($this->translate('No items added')));
}
}
$this->addElement($addButton);
$this->clearPopulatedValue('count');
$this->addElement('hidden', 'count', ['ignore' => true, 'value' => $newCount]);
}
/**
* Prepare nested dictionary for display
*
* @param array $nestedItems
* @param array $values
*
* @return array
*/
public static function prepare(array $nestedItems, array $values): array
{
$result = [];
foreach ($values as $key => $nestedValue) {
$nestedValue['key'] = $key;
$result[] = NestedDictionaryItem::prepare(
$nestedItems,
$nestedValue
);
}
return $result;
}
public function populate($values): static
{
if (! isset($values['count'])) {
$values['count'] = count($values);
}
return parent::populate($values);
}
/**
* Get the nested dictionary value
*
* @return array<int|string, DictionaryDataType>
*/
public function getDictionary(): array
{
$values = [];
$count = 0;
foreach ($this->ensureAssembled()->getElements() as $element) {
if ($element instanceof NestedDictionaryItem) {
$property = $element->getItem();
if (! empty($property['key']) && array_key_exists('value', $property)) {
$values[$property['key']] = $property['value'];
} else {
$values[self::UNDEFINED_KEY . $count] = $property['value'];
}
$count++;
}
}
return $values;
}
}