mirror of
https://github.com/Icinga/icingaweb2-module-director.git
synced 2026-06-04 22:32:55 -04:00
489 lines
18 KiB
PHP
489 lines
18 KiB
PHP
<?php
|
|
|
|
namespace Icinga\Module\Director\Forms\DictionaryElements;
|
|
|
|
use Icinga\Application\Config;
|
|
use Icinga\Module\Director\Db;
|
|
use Icinga\Module\Director\Db\DbUtil;
|
|
use Icinga\Module\Director\Forms\Validator\DatalistEntryValidator;
|
|
use Icinga\Module\Director\Web\Form\Element\ArrayElement;
|
|
use Icinga\Module\Director\Web\Form\Element\IplBoolean;
|
|
use ipl\Html\Attributes;
|
|
use ipl\Html\Contract\FormElement;
|
|
use ipl\Html\FormElement\FieldsetElement;
|
|
use ipl\Html\HtmlElement;
|
|
use ipl\Web\FormElement\TermInput;
|
|
use ipl\Web\Url;
|
|
use PDO;
|
|
use Ramsey\Uuid\Uuid;
|
|
use Ramsey\Uuid\UuidInterface;
|
|
|
|
/**
|
|
* @phpstan-type DictionaryItemDataType array{
|
|
* name: string,
|
|
* value: mixed
|
|
* }
|
|
*/
|
|
class DictionaryItem extends FieldsetElement
|
|
{
|
|
protected $defaultAttributes = ['class' => ['no-border', 'dictionary-item']];
|
|
|
|
/** @var array Dictionary Item Fields */
|
|
private $fields;
|
|
|
|
/** @var ?FormElement Remove button */
|
|
private ?FormElement $removeButton = null;
|
|
|
|
public function __construct(string $name, array $items, $attributes = null)
|
|
{
|
|
$this->fields = $items;
|
|
|
|
parent::__construct($name, $attributes);
|
|
}
|
|
|
|
private static function fetchItemType(UuidInterface $uuid): string
|
|
{
|
|
$db = Db::fromResourceName(Config::module('director')->get('db', 'resource'))->getDbAdapter();
|
|
$query = $db->select()
|
|
->from(
|
|
['dp' => 'director_property'],
|
|
['value_type' => 'dp.value_type']
|
|
)
|
|
->where('dp.parent_uuid = ?', Db\DbUtil::quoteBinaryCompat($uuid->getBytes(), $db));
|
|
|
|
return $db->fetchOne($query);
|
|
}
|
|
|
|
/**
|
|
* Fetch datalist entries for a given property uuid.
|
|
*
|
|
* @param UuidInterface $uuid
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function fetchDataListEntries(UuidInterface $uuid): array
|
|
{
|
|
$db = Db::fromResourceName(Config::module('director')->get('db', 'resource'))->getDbAdapter();
|
|
$query = $db->select()
|
|
->from(
|
|
['dle' => 'director_datalist_entry'],
|
|
['entry_name' => 'dle.entry_name', 'entry_value' => 'dle.entry_value']
|
|
)
|
|
->join(['dl' => 'director_datalist'], 'dl.id = dle.list_id', [])
|
|
->join(['dpl' => 'director_property_datalist'], 'dl.uuid = dpl.list_uuid', [])
|
|
->where('dpl.property_uuid = ?', Db\DbUtil::quoteBinaryCompat($uuid->getBytes(), $db));
|
|
|
|
return $db->fetchPairs($query);
|
|
}
|
|
|
|
protected function assemble(): void
|
|
{
|
|
if (empty($this->fields)) {
|
|
return;
|
|
}
|
|
|
|
$this->addElement('hidden', 'name', ['value' => $this->fields['key_name'] ?? '']);
|
|
$this->addElement('hidden', 'type', ['value' => $this->fields['value_type'] ?? '']);
|
|
$this->addElement('hidden', 'label', ['value' => $this->fields['label'] ?? '']);
|
|
$this->addElement('hidden', 'parent_type', ['value' => $this->fields['parent_type'] ?? '']);
|
|
|
|
$this->addElement('hidden', 'inherited');
|
|
$this->addElement('hidden', 'inherited_from');
|
|
|
|
$valElementName = 'var';
|
|
$type = $this->getElement('type')->getValue();
|
|
$label = $this->getElement('label')->getValue();
|
|
|
|
if ($this->removeButton !== null) {
|
|
$this->addAttributes(['class' => ['removable']]);
|
|
$this->addHtml(new HtmlElement(
|
|
'div',
|
|
null,
|
|
$this->removeButton
|
|
));
|
|
}
|
|
|
|
if ($label === null) {
|
|
$label = $this->getElement('name')->getValue();
|
|
}
|
|
|
|
$uuid = Uuid::fromBytes($this->fields['uuid']);
|
|
$children = static::fetchChildrenItems(
|
|
$uuid,
|
|
$this->fields['value_type'] ?? ''
|
|
);
|
|
$inherited = $this->getElement('inherited')->getValue();
|
|
$inheritedFrom = $this->getElement('inherited_from')->getValue();
|
|
|
|
$placeholder = '';
|
|
if ($inherited) {
|
|
$placeholder = $inherited . ' (' . sprintf($this->translate('Inherited from %s'), $inheritedFrom) . ')';
|
|
}
|
|
|
|
if ($type === 'number') {
|
|
$this->addElement(
|
|
'number',
|
|
$valElementName,
|
|
[
|
|
'label' => $label . ' (Number)',
|
|
'placeholder' => $placeholder,
|
|
'step' => 'any'
|
|
]
|
|
);
|
|
} elseif ($type == 'bool') {
|
|
$this->addElement(
|
|
new IplBoolean(
|
|
$valElementName,
|
|
['label' => $label, 'placeholder' => $placeholder]
|
|
)
|
|
);
|
|
} elseif ($type === 'dynamic-array') {
|
|
$this->addElement((new ArrayElement($valElementName))
|
|
->shouldAutoSubmit()
|
|
->setVerticalTermDirection()
|
|
->setPlaceHolder($placeholder)
|
|
->setLabel($label . ' (Array)'));
|
|
} elseif (str_starts_with($type, 'datalist-')) {
|
|
$isStrict = substr($type, strlen('datalist-')) === 'strict';
|
|
$itemType = self::fetchItemType($uuid);
|
|
$datalistEntries = self::fetchDataListEntries($uuid);
|
|
if ($itemType === 'string') {
|
|
if ($isStrict) {
|
|
$this->addElement(
|
|
'select',
|
|
$valElementName,
|
|
[
|
|
'label' => $label . ' (Datalist String [strict])',
|
|
'placeholder' => $placeholder,
|
|
'value' => '',
|
|
'options' => ['' => $this->translate('- Please choose -')]
|
|
+ $datalistEntries
|
|
]
|
|
);
|
|
} else {
|
|
$fieldsetName = $this->getName();
|
|
$listEntriesInput = $this->createElement('text', $valElementName, [
|
|
'autocomplete' => 'off',
|
|
'ignore' => true,
|
|
'label' => $label . ' (Datalist String [non-strict])',
|
|
'data-enrichment-type' => 'completion',
|
|
'data-auto-submit' => true,
|
|
'data-term-suggestions' => "#{$valElementName}-suggestions-{$fieldsetName}",
|
|
'data-suggest-url' => Url::fromPath('director/suggestions/datalist-entry', [
|
|
'uuid' => Uuid::fromBytes($this->fields['uuid'])->toString(),
|
|
'showCompact' => true,
|
|
'_disableLayout' => true
|
|
])
|
|
]);
|
|
|
|
$fieldset = new HtmlElement('fieldset');
|
|
$this->registerElement($listEntriesInput);
|
|
$searchInput = $this->createElement('hidden', "{$valElementName}-search", ['ignore' => true]);
|
|
$this->registerElement($searchInput);
|
|
$fieldset->addHtml($searchInput);
|
|
$labelInput = $this->createElement('hidden', "{$valElementName}-label", ['ignore' => true]);
|
|
$this->registerElement($labelInput);
|
|
$fieldset->addHtml($labelInput);
|
|
|
|
$this->decorate($listEntriesInput);
|
|
|
|
$fieldset->addHtml(
|
|
$listEntriesInput,
|
|
new HtmlElement('div', Attributes::create([
|
|
'id' => "{$valElementName}-suggestions-{$fieldsetName}",
|
|
'class' => 'search-suggestions'
|
|
]))
|
|
);
|
|
|
|
$this->addHtml($fieldset);
|
|
}
|
|
} elseif ($itemType === 'dynamic-array') {
|
|
$listEntriesInput = (new ArrayElement($valElementName))
|
|
->shouldAutoSubmit()
|
|
->setSuggestedValues($datalistEntries)
|
|
->setVerticalTermDirection()
|
|
->setSuggestionUrl(Url::fromPath('director/suggestions/datalist-entry', [
|
|
'uuid' => Uuid::fromBytes($this->fields['uuid'])->toString(),
|
|
'showCompact' => true,
|
|
'_disableLayout' => true
|
|
]));
|
|
|
|
if ($isStrict) {
|
|
$termValidator = function (array $terms) use ($datalistEntries) {
|
|
(new DatalistEntryValidator())
|
|
->setDatalistEntries($datalistEntries)
|
|
->isValid($terms);
|
|
};
|
|
|
|
$listEntriesInput
|
|
->setLabel($label . ' (Datalist Array [strict])')
|
|
->on(TermInput::ON_ENRICH, $termValidator)
|
|
->on(TermInput::ON_ADD, $termValidator)
|
|
->on(TermInput::ON_PASTE, $termValidator)
|
|
->on(TermInput::ON_SAVE, $termValidator);
|
|
} else {
|
|
$listEntriesInput->setLabel($label . ' (Datalist Array [non-strict])');
|
|
}
|
|
|
|
$this->addElement($listEntriesInput);
|
|
}
|
|
} elseif ($type === 'fixed-dictionary' || $type === 'fixed-array') {
|
|
$this->addElement(
|
|
(new Dictionary($valElementName, $children))
|
|
->setLabel($label . ' (' . ucfirst(substr($type, strlen('fixed-'))) . ')')
|
|
);
|
|
} elseif ($type === 'dynamic-dictionary') {
|
|
$this->addElement((new NestedDictionary(
|
|
$valElementName,
|
|
$children,
|
|
['inherited_from' => $inheritedFrom, 'value' => $inherited]
|
|
))->setLabel($label . ' (Dictionary)'));
|
|
} else {
|
|
$this->addElement(
|
|
'text',
|
|
$valElementName,
|
|
[
|
|
'label' => $label . ' (' . ucfirst($type) . ')',
|
|
'placeholder' => $placeholder
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
public function populate($values): void
|
|
{
|
|
if (empty($values)) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
$values['type'] === 'datalist-non-strict'
|
|
&& self::fetchItemType(Uuid::fromBytes($this->fields['uuid'])) === 'string'
|
|
) {
|
|
$datalistEntries = array_flip(self::fetchDataListEntries(Uuid::fromBytes($this->fields['uuid'])));
|
|
|
|
if (isset($datalistEntries[$values['var']])) {
|
|
$values['var-search'] = $datalistEntries[$values['var']];
|
|
$values['var-label'] = $values['var'];
|
|
} else {
|
|
$values['var-search'] = $values['var'];
|
|
}
|
|
}
|
|
|
|
parent::populate($values);
|
|
}
|
|
|
|
/**
|
|
* Prepare the dictionary item for display
|
|
*
|
|
* @param array $property
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function prepare(array $property): array
|
|
{
|
|
$values = [
|
|
'name' => $property['key_name'] ?? '',
|
|
'label' => $property['label'] ?? '',
|
|
'type' => $property['value_type'] ?? '',
|
|
'parent_type' => $property['parent_type'] ?? ''
|
|
];
|
|
|
|
$property['uuid'] = Dbutil::binaryResult($property['uuid'] ?? '');
|
|
|
|
if (
|
|
$property['value_type'] === 'dynamic-array'
|
|
|| (
|
|
in_array($property['value_type'], ['datalist-strict', 'datalist-non-strict'], true)
|
|
&& self::fetchItemType(Uuid::fromBytes($property['uuid'])) === 'dynamic-array'
|
|
)
|
|
) {
|
|
$values['var'] = $property['value'] ?? [];
|
|
$values['inherited'] = implode(', ', $property['inherited'] ?? []);
|
|
$values['inherited_from'] = $property['inherited_from'] ?? '';
|
|
} elseif ($property['value_type'] === 'fixed-dictionary' || $property['value_type'] === 'fixed-array') {
|
|
$childrenValues = ['value' => $property['value'] ?? []];
|
|
|
|
if (! isset($property['value'])) {
|
|
$childrenValues['inherited'] = $property['inherited'] ?? [];
|
|
$childrenValues['inherited_from'] = $property['inherited_from'] ?? '';
|
|
}
|
|
|
|
$dictionaryItems = static::fetchChildrenItems(
|
|
Uuid::fromBytes($property['uuid']),
|
|
$property['value_type'],
|
|
$childrenValues
|
|
);
|
|
$values['var'] = Dictionary::prepare($dictionaryItems);
|
|
} elseif ($property['value_type'] === 'dynamic-dictionary') {
|
|
$childrenValues = [
|
|
'value' => $property['value'] ?? [],
|
|
'inherited' => $property['inherited'] ?? [],
|
|
'inherited_from' => $property['inherited_from'] ?? ''
|
|
];
|
|
|
|
$dictionaryItems = static::fetchChildrenItems(
|
|
Uuid::fromBytes($property['uuid']),
|
|
$property['value_type'],
|
|
$childrenValues
|
|
);
|
|
$values['var'] = NestedDictionary::prepare(
|
|
$dictionaryItems,
|
|
$property['value'] ?? []
|
|
);
|
|
|
|
$values['inherited'] = isset($property['inherited'])
|
|
? json_encode($property['inherited'], JSON_PRETTY_PRINT)
|
|
: '';
|
|
$values['inherited_from'] = $property['inherited_from'] ?? '';
|
|
} elseif (
|
|
$property['value_type'] === 'datalist-non-strict'
|
|
&& self::fetchItemType(Uuid::fromBytes($property['uuid'])) === 'string'
|
|
) {
|
|
$dataListEntries = self::fetchDataListEntries(Uuid::fromBytes($property['uuid']));
|
|
$value = $property['value'] ?? '';
|
|
if (isset($dataListEntries[$value])) {
|
|
$values['var'] = $dataListEntries[$value];
|
|
$values['var-search'] = $value;
|
|
$values['var-label'] = $dataListEntries[$value];
|
|
} else {
|
|
$values['var'] = $value;
|
|
$values['var-search'] = $value;
|
|
}
|
|
} else {
|
|
$values['var'] = $property['value'] ?? '';
|
|
$values['inherited'] = $property['inherited'] ?? '';
|
|
$values['inherited_from'] = $property['inherited_from'] ?? '';
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* Fetch children items of the given parent item
|
|
*
|
|
* @param UuidInterface $parentUuid
|
|
* @param string $parentType
|
|
* @param array $values
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function fetchChildrenItems(UuidInterface $parentUuid, string $parentType, array $values = []): array
|
|
{
|
|
$db = Db::fromResourceName(Config::module('director')->get('db', 'resource'))->getDbAdapter();
|
|
|
|
$query = $db->select()
|
|
->from(
|
|
['dp' => 'director_property'],
|
|
[
|
|
'key_name' => 'dp.key_name',
|
|
'uuid' => 'dp.uuid',
|
|
'value_type' => 'dp.value_type',
|
|
'label' => 'dp.label',
|
|
'parent_uuid' => 'dp.parent_uuid',
|
|
'children' => 'COUNT(cdp.uuid)'
|
|
]
|
|
)
|
|
->where('dp.parent_uuid = ?', Db\DbUtil::quoteBinaryCompat($parentUuid->getBytes(), $db))
|
|
->joinLeft(
|
|
['cdp' => 'director_property'],
|
|
'cdp.parent_uuid = dp.uuid',
|
|
[]
|
|
)
|
|
->group(['dp.uuid', 'dp.key_name', 'dp.value_type', 'dp.label'])
|
|
->order('children')
|
|
->order('key_name');
|
|
|
|
$propertyItems = $db->fetchAll($query, fetchMode: PDO::FETCH_ASSOC);
|
|
foreach ($propertyItems as $key => $propertyItem) {
|
|
$propertyItem['uuid'] = DbUtil::binaryResult($propertyItem['uuid']);
|
|
$propertyItem['parent_uuid'] = DbUtil::binaryResult($propertyItem['parent_uuid']);
|
|
$propertyItems[$key] = $propertyItem;
|
|
}
|
|
|
|
if (empty($values)) {
|
|
return $propertyItems;
|
|
}
|
|
|
|
$result = [];
|
|
foreach ($propertyItems as $propertyItem) {
|
|
$propertyItem['parent_type'] = $parentType;
|
|
if (isset($values['value'][$propertyItem['key_name']])) {
|
|
$propertyItem['value'] = $values['value'][$propertyItem['key_name']];
|
|
}
|
|
|
|
if (isset($values['inherited'][$propertyItem['key_name']])) {
|
|
$propertyItem['inherited'] = $values['inherited'][$propertyItem['key_name']];
|
|
$propertyItem['inherited_from'] = $values['inherited_from'];
|
|
}
|
|
|
|
$result[$propertyItem['key_name']] = $propertyItem;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Set the remove button.
|
|
*
|
|
* @param ?FormElement $removeButton
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setRemoveButton(?FormElement $removeButton): static
|
|
{
|
|
$this->removeButton = $removeButton;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get the dictionary item value
|
|
*
|
|
* @return DictionaryItemDataType
|
|
*/
|
|
public function getItem(): array
|
|
{
|
|
$values = ['name' => $this->getElement('name')->getValue()];
|
|
$itemValue = $this->getElement('var');
|
|
if ($itemValue instanceof NestedDictionary or $itemValue instanceof Dictionary) {
|
|
$values['value'] = $itemValue->getDictionary();
|
|
|
|
if ($this->getElement('type')->getValue() === 'fixed-array') {
|
|
$value = $values['value'];
|
|
ksort($value);
|
|
$values['value'] = array_values($value);
|
|
}
|
|
} elseif (
|
|
$this->getElement('type')->getValue() === 'datalist-non-strict'
|
|
&& self::fetchItemType(Uuid::fromBytes($this->fields['uuid'])) === 'string'
|
|
) {
|
|
$values['value'] = $this->getElement('var-search')->getValue();
|
|
} else {
|
|
if (! empty($this->getElement('inherited')->getValue())) {
|
|
$values['value'] = $itemValue->getValue();
|
|
} else {
|
|
$defaultValue = null;
|
|
|
|
// Use the default value for fixed-array items only if the fixed array does not have an inherited value
|
|
if ($this->getElement('parent_type')->getValue() === 'fixed-array') {
|
|
match ($this->getElement('type')->getValue()) {
|
|
'string' => $defaultValue = '',
|
|
'number' => $defaultValue = 0,
|
|
'bool' => $defaultValue = 'n',
|
|
'fixed-array', 'dynamic-array' => $defaultValue = []
|
|
};
|
|
}
|
|
|
|
$values['value'] = $itemValue->getValue() ?? $defaultValue;
|
|
}
|
|
}
|
|
|
|
$markForRemovalElement = 'delete-' . $this->getName();
|
|
if ($this->hasElement($markForRemovalElement)) {
|
|
$values['delete'] = $this->getElement($markForRemovalElement)->getValue();
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
}
|