mirror of
https://github.com/Icinga/icingaweb2-module-director.git
synced 2026-06-04 22:32:55 -04:00
Introduce a first-class DirectorProperty concept that extends the existing
data-fields model with rich, structured variable types. Custom variables can
now be defined globally under a new "Custom Variables" section and assigned
to Icinga objects (hosts, services, commands, etc.).
Supported types: string, number, boolean, fixed-array, dynamic-array,
datalist-strict, datalist-non-strict, fixed-dictionary, and
dynamic-dictionary (one level of nesting allowed).
Key changes:
- DirectorProperty object with CRUD, inheritance, and apply-for rule support
- New form elements: Dictionary, DictionaryItem, NestedDictionary,
NestedDictionaryItem, ArrayElement, IplBoolean
- CustomvarController for managing global property definitions
- VariablesController for per-object variable assignment
- BasketSnapshotCustomVariableResolver for basket import/export of properties
- REST API: IcingaObjectHandler extended to expose and accept structured vars
- IcingaConfigHelper: renders dictionaries/arrays to valid Icinga 2 DSL
- CustomVarRenderer updated for icingadb hook display
- DB migration upgrade_192.sql: new director_property and related tables
- CLI: MigrateCommand for migrating legacy vars to properties;
HostsCommand for bulk host custom variable management
183 lines
5.5 KiB
PHP
183 lines
5.5 KiB
PHP
<?php
|
|
|
|
namespace Icinga\Module\Director\Forms\DictionaryElements;
|
|
|
|
use ipl\Html\FormElement\FieldsetElement;
|
|
use ipl\Web\Widget\EmptyStateBar;
|
|
|
|
/**
|
|
* @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 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);
|
|
}
|
|
|
|
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
|
|
]);
|
|
|
|
$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) {
|
|
$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;
|
|
}
|
|
}
|