Add auto completion (#387)

resolves #295 

requires https://github.com/Icinga/ipl-web/pull/179
requires https://github.com/Icinga/ipl-web/pull/178
This commit is contained in:
Johannes Meyer 2023-08-10 12:43:11 +02:00 committed by GitHub
commit a30f85dbcc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1321 additions and 1117 deletions

View file

@ -6,6 +6,8 @@ use Icinga\Application\Modules\Module;
use Icinga\Date\DateFormatter;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\Forms\AddNodeForm;
use Icinga\Module\Businessprocess\Forms\EditNodeForm;
use Icinga\Module\Businessprocess\Node;
use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
use Icinga\Module\Businessprocess\Renderer\Breadcrumb;
@ -269,13 +271,25 @@ class ProcessController extends Controller
$canEdit = $bp->getMetadata()->canModify();
if ($action === 'add' && $canEdit) {
$form = $this->loadForm('AddNode')
->setSuccessUrl(Url::fromRequest()->without('action'))
->setStorage($this->storage())
$form = (new AddNodeForm())
->setProcess($bp)
->setParentNode($node)
->setStorage($this->storage())
->setSession($this->session())
->handleRequest();
->on(AddNodeForm::ON_SUCCESS, function () {
$this->redirectNow(Url::fromRequest()->without('action'));
})
->handleRequest($this->getServerRequest());
if ($form->hasElement('children')) {
foreach ($form->getElement('children')->prepareMultipartUpdate($this->getServerRequest()) as $update) {
if (! is_array($update)) {
$update = [$update];
}
$this->addPart(...$update);
}
}
} elseif ($action === 'cleanup' && $canEdit) {
$form = $this->loadForm('CleanupNode')
->setSuccessUrl(Url::fromRequest()->without('action'))
@ -283,13 +297,15 @@ class ProcessController extends Controller
->setSession($this->session())
->handleRequest();
} elseif ($action === 'editmonitored' && $canEdit) {
$form = $this->loadForm('EditNode')
->setSuccessUrl(Url::fromRequest()->without('action'))
$form = (new EditNodeForm())
->setProcess($bp)
->setNode($bp->getNode($this->params->get('editmonitorednode')))
->setParentNode($node)
->setSession($this->session())
->handleRequest();
->on(EditNodeForm::ON_SUCCESS, function () {
$this->redirectNow(Url::fromRequest()->without(['action', 'editmonitorednode']));
})
->handleRequest($this->getServerRequest());
} elseif ($action === 'delete' && $canEdit) {
$form = $this->loadForm('DeleteNode')
->setSuccessUrl(Url::fromRequest()->without('action'))
@ -349,8 +365,11 @@ class ProcessController extends Controller
return;
}
if ($this->params->get('action')) {
$this->setAutorefreshInterval(45);
if ($this->params->has('action')) {
if ($this->params->get('action') !== 'add') {
// The new add form uses the term input, which doesn't support value persistence across refreshes
$this->setAutorefreshInterval(45);
}
} else {
$this->setAutorefreshInterval(10);
}

View file

@ -0,0 +1,372 @@
<?php
namespace Icinga\Module\Businessprocess\Controllers;
use Exception;
use Icinga\Data\Filter\Filter as LegacyFilter;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\HostNode;
use Icinga\Module\Businessprocess\IcingaDbObject;
use Icinga\Module\Businessprocess\ImportedNode;
use Icinga\Module\Businessprocess\Monitoring\DataView\HostStatus;
use Icinga\Module\Businessprocess\Monitoring\DataView\ServiceStatus;
use Icinga\Module\Businessprocess\MonitoringRestrictions;
use Icinga\Module\Businessprocess\ServiceNode;
use Icinga\Module\Businessprocess\Web\Controller;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\Service;
use ipl\Stdlib\Filter;
use ipl\Web\FormElement\TermInput\TermSuggestions;
class SuggestionsController extends Controller
{
public function processAction()
{
$ignoreList = [];
$forConfig = null;
$forParent = null;
if ($this->params->has('config')) {
$forConfig = $this->loadModifiedBpConfig();
$parentName = $this->params->get('node');
if ($parentName) {
$forParent = $forConfig->getBpNode($parentName);
$collectParents = function ($node) use ($ignoreList, &$collectParents) {
foreach ($node->getParents() as $parent) {
$ignoreList[$parent->getName()] = true;
if ($parent->hasParents()) {
$collectParents($parent);
}
}
};
$ignoreList[$parentName] = true;
if ($forParent->hasParents()) {
$collectParents($forParent);
}
foreach ($forParent->getChildNames() as $name) {
$ignoreList[$name] = true;
}
}
}
$suggestions = new TermSuggestions((function () use ($forConfig, $forParent, $ignoreList, &$suggestions) {
foreach ($this->storage()->listProcessNames() as $config) {
$differentConfig = false;
if ($forConfig === null || $config !== $forConfig->getName()) {
if ($forConfig !== null && $forParent === null) {
continue;
}
try {
$bp = $this->storage()->loadProcess($config);
} catch (Exception $_) {
continue;
}
$differentConfig = true;
} else {
$bp = $forConfig;
}
foreach ($bp->getBpNodes() as $bpNode) {
/** @var BpNode $bpNode */
if ($bpNode instanceof ImportedNode) {
continue;
}
$search = $bpNode->getName();
if ($differentConfig) {
$search = "@$config:$search";
}
if (in_array($search, $suggestions->getExcludeTerms(), true)
|| isset($ignoreList[$search])
|| ($forParent
? $forParent->hasChild($search)
: ($forConfig && $forConfig->hasRootNode($search))
)
) {
continue;
}
if ($suggestions->matchSearch($bpNode->getName())
|| (! $bpNode->hasAlias() || $suggestions->matchSearch($bpNode->getAlias()))
|| $bpNode->getName() === $suggestions->getOriginalSearchValue()
|| $bpNode->getAlias() === $suggestions->getOriginalSearchValue()
) {
yield [
'search' => $search,
'label' => $bpNode->getAlias() ?? $bpNode->getName(),
'config' => $config
];
}
}
}
})());
$suggestions->setGroupingCallback(function (array $data) {
return $this->storage()->loadMetadata($data['config'])->getTitle();
});
$this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest()));
}
public function icingadbHostAction()
{
$excludes = Filter::none();
$forConfig = null;
if ($this->params->has('config')) {
$forConfig = $this->loadModifiedBpConfig();
if ($this->params->has('node')) {
$nodeName = $this->params->get('node');
$node = $forConfig->getBpNode($nodeName);
foreach ($node->getChildren() as $child) {
if ($child instanceof HostNode) {
$excludes->add(Filter::equal('host.name', $child->getHostname()));
}
}
}
}
$suggestions = new TermSuggestions((function () use ($forConfig, $excludes, &$suggestions) {
foreach ($suggestions->getExcludeTerms() as $excludeTerm) {
[$hostName, $_] = BpConfig::splitNodeName($excludeTerm);
$excludes->add(Filter::equal('host.name', $hostName));
}
$hosts = Host::on($forConfig->getBackend())
->columns(['host.name', 'host.display_name'])
->limit(50);
IcingaDbObject::applyIcingaDbRestrictions($hosts);
$hosts->filter(Filter::all(
$excludes,
Filter::any(
Filter::like('host.name', $suggestions->getSearchTerm()),
Filter::equal('host.name', $suggestions->getOriginalSearchValue()),
Filter::like('host.display_name', $suggestions->getSearchTerm()),
Filter::equal('host.display_name', $suggestions->getOriginalSearchValue()),
Filter::like('host.address', $suggestions->getSearchTerm()),
Filter::equal('host.address', $suggestions->getOriginalSearchValue()),
Filter::like('host.address6', $suggestions->getSearchTerm()),
Filter::equal('host.address6', $suggestions->getOriginalSearchValue()),
Filter::like('host.customvar_flat.flatvalue', $suggestions->getSearchTerm()),
Filter::equal('host.customvar_flat.flatvalue', $suggestions->getOriginalSearchValue()),
Filter::like('hostgroup.name', $suggestions->getSearchTerm()),
Filter::equal('hostgroup.name', $suggestions->getOriginalSearchValue())
)
));
foreach ($hosts as $host) {
yield [
'search' => BpConfig::joinNodeName($host->name, 'Hoststatus'),
'label' => $host->display_name,
'class' => 'host'
];
}
})());
$this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest()));
}
public function icingadbServiceAction()
{
$excludes = Filter::none();
$forConfig = null;
if ($this->params->has('config')) {
$forConfig = $this->loadModifiedBpConfig();
if ($this->params->has('node')) {
$nodeName = $this->params->get('node');
$node = $forConfig->getBpNode($nodeName);
foreach ($node->getChildren() as $child) {
if ($child instanceof ServiceNode) {
$excludes->add(Filter::all(
Filter::equal('host.name', $child->getHostname()),
Filter::equal('service.name', $child->getServiceDescription())
));
}
}
}
}
$suggestions = new TermSuggestions((function () use ($forConfig, $excludes, &$suggestions) {
foreach ($suggestions->getExcludeTerms() as $excludeTerm) {
[$hostName, $serviceName] = BpConfig::splitNodeName($excludeTerm);
if ($serviceName !== null && $serviceName !== 'Hoststatus') {
$excludes->add(Filter::all(
Filter::equal('host.name', $hostName),
Filter::equal('service.name', $serviceName)
));
}
}
$services = Service::on($forConfig->getBackend())
->columns(['host.name', 'host.display_name', 'service.name', 'service.display_name'])
->limit(50);
IcingaDbObject::applyIcingaDbRestrictions($services);
$services->filter(Filter::all(
$excludes,
Filter::any(
Filter::like('host.name', $suggestions->getSearchTerm()),
Filter::equal('host.name', $suggestions->getOriginalSearchValue()),
Filter::like('host.display_name', $suggestions->getSearchTerm()),
Filter::equal('host.display_name', $suggestions->getOriginalSearchValue()),
Filter::like('service.name', $suggestions->getSearchTerm()),
Filter::equal('service.name', $suggestions->getOriginalSearchValue()),
Filter::like('service.display_name', $suggestions->getSearchTerm()),
Filter::equal('service.display_name', $suggestions->getOriginalSearchValue()),
Filter::like('service.customvar_flat.flatvalue', $suggestions->getSearchTerm()),
Filter::equal('service.customvar_flat.flatvalue', $suggestions->getOriginalSearchValue()),
Filter::like('servicegroup.name', $suggestions->getSearchTerm()),
Filter::equal('servicegroup.name', $suggestions->getOriginalSearchValue())
)
));
foreach ($services as $service) {
yield [
'class' => 'service',
'search' => BpConfig::joinNodeName($service->host->name, $service->name),
'label' => sprintf(
$this->translate('%s on %s', '<service> on <host>'),
$service->display_name,
$service->host->display_name
)
];
}
})());
$this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest()));
}
public function monitoringHostAction()
{
$excludes = LegacyFilter::matchAny();
$forConfig = null;
if ($this->params->has('config')) {
$forConfig = $this->loadModifiedBpConfig();
if ($this->params->has('node')) {
$nodeName = $this->params->get('node');
$node = $forConfig->getBpNode($nodeName);
foreach ($node->getChildren() as $child) {
if ($child instanceof HostNode) {
$excludes->addFilter(LegacyFilter::where('host_name', $child->getHostname()));
}
}
}
}
$suggestions = new TermSuggestions((function () use ($forConfig, $excludes, &$suggestions) {
foreach ($suggestions->getExcludeTerms() as $excludeTerm) {
[$hostName, $_] = BpConfig::splitNodeName($excludeTerm);
$excludes->addFilter(LegacyFilter::where('host_name', $hostName));
}
$hosts = (new HostStatus($forConfig->getBackend()->select(), ['host_name', 'host_display_name']))
->limit(50)
->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
->applyFilter(LegacyFilter::matchAny(
LegacyFilter::where('host_name', $suggestions->getSearchTerm()),
LegacyFilter::where('host_display_name', $suggestions->getSearchTerm()),
LegacyFilter::where('host_address', $suggestions->getSearchTerm()),
LegacyFilter::where('host_address6', $suggestions->getSearchTerm()),
LegacyFilter::where('_host_%', $suggestions->getSearchTerm()),
// This also forces a group by on the query, needed anyway due to the custom var filter
// above, which may return multiple rows because of the wildcard in the name filter.
LegacyFilter::where('hostgroup_name', $suggestions->getSearchTerm()),
LegacyFilter::where('hostgroup_alias', $suggestions->getSearchTerm())
));
if (! $excludes->isEmpty()) {
$hosts->applyFilter(LegacyFilter::not($excludes));
}
foreach ($hosts as $row) {
yield [
'search' => BpConfig::joinNodeName($row->host_name, 'Hoststatus'),
'label' => $row->host_display_name,
'class' => 'host'
];
}
})());
$this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest()));
}
public function monitoringServiceAction()
{
$excludes = LegacyFilter::matchAny();
$forConfig = null;
if ($this->params->has('config')) {
$forConfig = $this->loadModifiedBpConfig();
if ($this->params->has('node')) {
$nodeName = $this->params->get('node');
$node = $forConfig->getBpNode($nodeName);
foreach ($node->getChildren() as $child) {
if ($child instanceof ServiceNode) {
$excludes->addFilter(LegacyFilter::matchAll(
LegacyFilter::where('host_name', $child->getHostname()),
LegacyFilter::where('service_description', $child->getServiceDescription())
));
}
}
}
}
$suggestions = new TermSuggestions((function () use ($forConfig, $excludes, &$suggestions) {
foreach ($suggestions->getExcludeTerms() as $excludeTerm) {
[$hostName, $serviceName] = BpConfig::splitNodeName($excludeTerm);
if ($serviceName !== null && $serviceName !== 'Hoststatus') {
$excludes->addFilter(LegacyFilter::matchAll(
LegacyFilter::where('host_name', $hostName),
LegacyFilter::where('service_description', $serviceName)
));
}
}
$services = (new ServiceStatus($forConfig->getBackend()->select(), [
'host_name',
'host_display_name',
'service_description',
'service_display_name'
]))
->limit(50)
->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
->applyFilter(LegacyFilter::matchAny(
LegacyFilter::where('host_name', $suggestions->getSearchTerm()),
LegacyFilter::where('host_display_name', $suggestions->getSearchTerm()),
LegacyFilter::where('service_description', $suggestions->getSearchTerm()),
LegacyFilter::where('service_display_name', $suggestions->getSearchTerm()),
LegacyFilter::where('_service_%', $suggestions->getSearchTerm()),
// This also forces a group by on the query, needed anyway due to the custom var filter
// above, which may return multiple rows because of the wildcard in the name filter.
LegacyFilter::where('servicegroup_name', $suggestions->getSearchTerm()),
LegacyFilter::where('servicegroup_alias', $suggestions->getSearchTerm())
));
if (! $excludes->isEmpty()) {
$services->applyFilter(LegacyFilter::not($excludes));
}
foreach ($services as $row) {
yield [
'class' => 'service',
'search' => BpConfig::joinNodeName($row->host_name, $row->service_description),
'label' => sprintf(
$this->translate('%s on %s', '<service> on <host>'),
$row->service_display_name,
$row->host_display_name
)
];
}
})());
$this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest()));
}
}

View file

@ -5,509 +5,406 @@ namespace Icinga\Module\Businessprocess\Forms;
use Exception;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\Common\EnumList;
use Icinga\Module\Businessprocess\Common\Sort;
use Icinga\Module\Businessprocess\ImportedNode;
use Icinga\Module\Businessprocess\Modification\ProcessChanges;
use Icinga\Module\Businessprocess\Node;
use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
use Icinga\Module\Businessprocess\Web\Form\Validator\NoDuplicateChildrenValidator;
use Icinga\Module\Businessprocess\Storage\Storage;
use Icinga\Module\Businessprocess\Web\Form\Element\IplStateOverrides;
use Icinga\Module\Businessprocess\Web\Form\Validator\HostServiceTermValidator;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Web\Session\SessionNamespace;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\I18n\Translation;
use ipl\Stdlib\Str;
use ipl\Web\Compat\CompatForm;
use ipl\Web\FormElement\TermInput;
use ipl\Web\Url;
class AddNodeForm extends BpConfigBaseForm
class AddNodeForm extends CompatForm
{
use EnumList;
use Sort;
use Translation;
/** @var BpNode */
/** @var Storage */
protected $storage;
/** @var ?BpConfig */
protected $bp;
/** @var ?BpNode */
protected $parent;
protected $objectList = array();
protected $processList = array();
public function setup()
{
$view = $this->getView();
if ($this->hasParentNode()) {
$this->addHtml(
'<h2>' . $view->escape(
sprintf($this->translate('Add a node to %s'), $this->parent->getAlias())
) . '</h2>'
);
} else {
$this->addHtml(
'<h2>' . $this->translate('Add a new root node') . '</h2>'
);
}
$type = $this->selectNodeType();
switch ($type) {
case 'host':
$this->selectHost();
break;
case 'service':
$this->selectService();
break;
case 'process':
$this->selectProcess();
break;
case 'new-process':
$this->addNewProcess();
break;
case 'hosts_from_filter':
$this->selectHostsFromFilter();
break;
case 'services_from_filter':
$this->selectServicesFromFilter();
break;
case null:
$this->setSubmitLabel($this->translate('Next'));
return;
}
}
protected function addNewProcess()
{
$this->addElement('text', 'name', array(
'label' => $this->translate('ID'),
'required' => true,
'description' => $this->translate(
'This is the unique identifier of this process'
),
'validators' => [
['Callback', true, [
'callback' => function ($value) {
if ($this->hasParentNode()) {
return ! $this->parent->hasChild($value);
}
return ! $this->bp->hasRootNode($value);
},
'messages' => [
'callbackValue' => $this->translate('%value% is already defined in this process')
]
]]
]
));
$this->addElement('text', 'alias', array(
'label' => $this->translate('Display Name'),
'description' => $this->translate(
'Usually this name will be shown for this node. Equals ID'
. ' if not given'
),
));
$this->addElement('select', 'operator', array(
'label' => $this->translate('Operator'),
'required' => true,
'multiOptions' => Node::getOperators()
));
$display = 1;
if ($this->bp->getMetadata()->isManuallyOrdered() && ! $this->bp->isEmpty()) {
$rootNodes = self::applyManualSorting($this->bp->getRootNodes());
$display = end($rootNodes)->getDisplay() + 1;
}
$this->addElement('select', 'display', array(
'label' => $this->translate('Visualization'),
'required' => true,
'description' => $this->translate(
'Where to show this process'
),
'value' => $this->hasParentNode() ? '0' : "$display",
'multiOptions' => array(
"$display" => $this->translate('Toplevel Process'),
'0' => $this->translate('Subprocess only'),
)
));
$this->addElement('text', 'infoUrl', array(
'label' => $this->translate('Info URL'),
'description' => $this->translate(
'URL pointing to more information about this node'
)
));
}
/** @var SessionNamespace */
protected $session;
/**
* @return string|null
*/
protected function selectNodeType()
{
$types = array();
if ($this->hasParentNode()) {
$types['host'] = $this->translate('Host');
$types['service'] = $this->translate('Service');
$types['hosts_from_filter'] = $this->translate('Hosts from filter');
$types['services_from_filter'] = $this->translate('Services from filter');
} elseif (! $this->hasProcesses()) {
$this->addElement('hidden', 'node_type', array(
'ignore' => true,
'decorators' => array('ViewHelper'),
'value' => 'new-process'
));
return 'new-process';
}
if ($this->hasProcesses() || ($this->hasParentNode() && $this->hasMoreConfigs())) {
$types['process'] = $this->translate('Existing Process');
}
$types['new-process'] = $this->translate('New Process Node');
$this->addElement('select', 'node_type', array(
'label' => $this->translate('Node type'),
'required' => true,
'description' => $this->translate(
'The node type you want to add'
),
'ignore' => true,
'class' => 'autosubmit',
'multiOptions' => $this->optionalEnum($types)
));
return $this->getSentValue('node_type');
}
protected function selectHost()
{
$this->addElement('multiselect', 'children', [
'label' => $this->translate('Hosts'),
'required' => true,
'size' => 8,
'multiOptions' => $this->enumHostList(),
'description' => $this->translate(
'Hosts that should be part of this business process node'
),
'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]]
]);
$this->addHostOverrideCheckbox();
if ($this->getSentValue('host_override') === '1') {
$this->addHostOverrideElement();
}
}
protected function selectService()
{
$this->addHostElement();
if ($host = $this->getSentValue('host')) {
$this->addServicesElement($host);
$this->addServiceOverrideCheckbox();
if ($this->getSentValue('service_override') === '1') {
$this->addServiceOverrideElement();
}
} else {
$this->setSubmitLabel($this->translate('Next'));
}
}
protected function addHostElement()
{
$this->addElement('select', 'host', array(
'label' => $this->translate('Host'),
'required' => true,
'ignore' => true,
'class' => 'autosubmit',
'multiOptions' => $this->optionalEnum($this->enumHostForServiceList()),
));
}
protected function addServicesElement($host)
{
$this->addElement('multiselect', 'children', [
'label' => $this->translate('Services'),
'required' => true,
'size' => 8,
'multiOptions' => $this->enumServiceList($host),
'description' => $this->translate(
'Services that should be part of this business process node'
),
'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]]
]);
}
protected function addFilteredHostsElement($filter)
{
$this->addElement('submit', 'refresh', [
'label' => $this->translate('Refresh'),
'class' => 'refresh-filter'
]);
$this->addElement('multiselect', 'children', [
'label' => $this->translate('Hosts'),
'required' => true,
'size' => 8,
'multiOptions' => $this->enumHostListByFilter($filter),
'description' => $this->translate(
'Hosts that should be part of this business process node'
),
'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]]
]);
}
protected function addFilteredServicesElement($filter)
{
$this->addElement('submit', 'refresh', [
'label' => $this->translate('Refresh'),
'class' => 'refresh-filter'
]);
$this->addElement('multiselect', 'children', [
'label' => $this->translate('Services'),
'required' => true,
'size' => 8,
'multiOptions' => $this->enumServiceListByFilter($filter),
'description' => $this->translate(
'Services that should be part of this business process node'
),
'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]]
]);
}
protected function addFilterElement()
{
$this->addElement('text', 'filter', array(
'label' => $this->translate('Filter'),
'required' => true,
'ignore' => true
));
}
protected function addFileElement()
{
$this->addElement('select', 'file', [
'label' => $this->translate('File'),
'required' => true,
'ignore' => true,
'value' => $this->bp->getName(),
'class' => 'autosubmit',
'multiOptions' => $this->optionalEnum($this->enumConfigs()),
'description' => $this->translate(
'Choose a different configuration file to import its processes'
)
]);
}
protected function addHostOverrideCheckbox()
{
$this->addElement('checkbox', 'host_override', [
'ignore' => true,
'class' => 'autosubmit',
'label' => $this->translate('Override Host State'),
'description' => $this->translate('Enable host state overrides')
]);
}
protected function addHostOverrideElement()
{
$this->addElement('stateOverrides', 'stateOverrides', [
'required' => true,
'label' => $this->translate('State Overrides'),
'states' => $this->enumHostStateList()
]);
}
protected function addServiceOverrideCheckbox()
{
$this->addElement('checkbox', 'service_override', [
'ignore' => true,
'class' => 'autosubmit',
'label' => $this->translate('Override Service State'),
'description' => $this->translate('Enable service state overrides')
]);
}
protected function addServiceOverrideElement()
{
$this->addElement('stateOverrides', 'stateOverrides', [
'required' => true,
'label' => $this->translate('State Overrides'),
'states' => $this->enumServiceStateList()
]);
}
protected function selectHostsFromFilter()
{
$this->addFilterElement();
if ($filter = $this->getSentValue('filter')) {
$this->addFilteredHostsElement($filter);
} else {
$this->setSubmitLabel($this->translate('Next'));
}
}
protected function selectServicesFromFilter()
{
$this->addFilterElement();
if ($filter = $this->getSentValue('filter')) {
$this->addFilteredServicesElement($filter);
} else {
$this->setSubmitLabel($this->translate('Next'));
}
}
protected function selectProcess()
{
if ($this->hasParentNode()) {
$this->addFileElement();
}
if (($file = $this->getSentValue('file')) || !$this->hasParentNode()) {
$this->addElement('multiselect', 'children', [
'label' => $this->translate('Process nodes'),
'required' => true,
'size' => 8,
'multiOptions' => $this->enumProcesses($file),
'description' => $this->translate(
'Other processes that should be part of this business process node'
),
'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]]
]);
} else {
$this->setSubmitLabel($this->translate('Next'));
}
}
/**
* @param BpNode|null $node
* Set the storage to use
*
* @param Storage $storage
*
* @return $this
*/
public function setParentNode(BpNode $node = null)
public function setStorage(Storage $storage): self
{
$this->parent = $node;
$this->storage = $storage;
return $this;
}
/**
* @return bool
* Set the affected configuration
*
* @param BpConfig $bp
*
* @return $this
*/
public function hasParentNode()
public function setProcess(BpConfig $bp): self
{
return $this->parent !== null;
}
$this->bp = $bp;
protected function hasProcesses()
{
return count($this->enumProcesses()) > 0;
return $this;
}
/**
* @param string $file
* @return array
* Set the affected sub-process
*
* @param ?BpNode $node
*
* @return $this
*/
protected function enumProcesses($file = null)
public function setParentNode(BpNode $node = null): self
{
$list = array();
$this->parent = $node;
$parents = array();
return $this;
}
$differentFile = $file !== null && $file !== $this->bp->getName();
/**
* Set the user's session
*
* @param SessionNamespace $session
*
* @return $this
*/
public function setSession(SessionNamespace $session): self
{
$this->session = $session;
if (! $differentFile && $this->hasParentNode()) {
$this->collectAllParents($this->parent, $parents);
$parents[$this->parent->getName()] = $this->parent;
}
return $this;
}
$bp = $this->bp;
if ($differentFile) {
try {
$bp = $this->storage->loadProcess($file);
} catch (Exception $e) {
$this->addError('Cannot add invalid config file');
return $list;
protected function assemble()
{
if ($this->parent !== null) {
$title = sprintf($this->translate('Add a node to %s'), $this->parent->getAlias());
$nodeTypes = [
'host' => $this->translate('Host'),
'service' => $this->translate('Service'),
'process' => $this->translate('Existing Process'),
'new-process' => $this->translate('New Process')
];
} else {
$title = $this->translate('Add a new root node');
if (! $this->bp->isEmpty()) {
$nodeTypes = [
'process' => $this->translate('Existing Process'),
'new-process' => $this->translate('New Process')
];
} else {
$nodeTypes = [];
}
}
foreach ($bp->getNodes() as $node) {
if (! $node instanceof ImportedNode && $node instanceof BpNode && ! isset($parents[$node->getName()])) {
$name = $node->getName();
if ($differentFile) {
$name = '@' . $file . ':' . $name;
$this->addHtml(new HtmlElement('h2', null, Text::create($title)));
if (! empty($nodeTypes)) {
$this->addElement('select', 'node_type', [
'label' => $this->translate('Node type'),
'options' => array_merge(
['' => ' - ' . $this->translate('Please choose') . ' - '],
$nodeTypes
),
'disabledOptions' => [''],
'class' => 'autosubmit',
'required' => true,
'ignore' => true
]);
$nodeType = $this->getPopulatedValue('node_type');
} else {
$nodeType = 'new-process';
}
if ($nodeType === 'new-process') {
$this->assembleNewProcessElements();
} elseif ($nodeType === 'process') {
$this->assembleExistingProcessElements();
} elseif ($nodeType === 'host') {
$this->assembleHostElements();
} elseif ($nodeType === 'service') {
$this->assembleServiceElements();
}
$this->addElement('submit', 'submit', [
'label' => $this->translate('Add Process')
]);
}
protected function assembleNewProcessElements(): void
{
$this->addElement('text', 'name', [
'required' => true,
'ignore' => true,
'label' => $this->translate('ID'),
'description' => $this->translate('This is the unique identifier of this process'),
'validators' => [
'callback' => function ($value, $validator) {
if ($this->parent !== null ? $this->parent->hasChild($value) : $this->bp->hasRootNode($value)) {
$validator->addMessage(
sprintf($this->translate('%s is already defined in this process'), $value)
);
return false;
}
return true;
}
]
]);
$this->addElement('text', 'alias', [
'label' => $this->translate('Display Name'),
'description' => $this->translate(
'Usually this name will be shown for this node. Equals ID if not given'
),
]);
$this->addElement('select', 'operator', [
'required' => true,
'label' => $this->translate('Operator'),
'multiOptions' => Node::getOperators()
]);
$display = 1;
if (! $this->bp->isEmpty() && $this->bp->getMetadata()->isManuallyOrdered()) {
$rootNodes = self::applyManualSorting($this->bp->getRootNodes());
$display = end($rootNodes)->getDisplay() + 1;
}
$this->addElement('select', 'display', [
'required' => true,
'label' => $this->translate('Visualization'),
'description' => $this->translate('Where to show this process'),
'value' => $this->parent !== null ? '0' : "$display",
'multiOptions' => [
"$display" => $this->translate('Toplevel Process'),
'0' => $this->translate('Subprocess only'),
]
]);
$this->addElement('text', 'infoUrl', [
'label' => $this->translate('Info URL'),
'description' => $this->translate('URL pointing to more information about this node')
]);
}
protected function assembleExistingProcessElements(): void
{
$termValidator = function (array $terms) {
foreach ($terms as $term) {
/** @var TermInput\ValidatedTerm $term */
$nodeName = $term->getSearchValue();
if ($nodeName[0] === '@') {
if ($this->parent === null) {
$term->setMessage($this->translate('Imported nodes cannot be used as root nodes'));
} elseif (strpos($nodeName, ':') === false) {
$term->setMessage($this->translate('Missing node name'));
} else {
[$config, $nodeName] = Str::trimSplit(substr($nodeName, 1), ':', 2);
if (! $this->storage->hasProcess($config)) {
$term->setMessage($this->translate('Config does not exist or access has been denied'));
} else {
try {
$bp = $this->storage->loadProcess($config);
} catch (Exception $e) {
$term->setMessage(
sprintf($this->translate('Cannot load config: %s'), $e->getMessage())
);
}
if (isset($bp)) {
if (! $bp->hasNode($nodeName)) {
$term->setMessage($this->translate('No node with this name found in config'));
} else {
$term->setLabel($bp->getNode($nodeName)->getAlias());
}
}
}
}
} elseif (! $this->bp->hasNode($nodeName)) {
$term->setMessage($this->translate('No node with this name found in config'));
} else {
$term->setLabel($this->bp->getNode($nodeName)->getAlias());
}
$list[$name] = $node->getName(); // display name?
if ($this->parent !== null && $this->parent->hasChild($term->getSearchValue())) {
$term->setMessage($this->translate('Already defined in this process'));
}
if ($this->parent !== null && $term->getSearchValue() === $this->parent->getName()) {
$term->setMessage($this->translate('Results in a parent/child loop'));
}
}
};
$this->addElement(
(new TermInput('children'))
->setRequired()
->setVerticalTermDirection()
->setLabel($this->translate('Process Nodes'))
->setSuggestionUrl(Url::fromPath('businessprocess/suggestions/process', [
'node' => isset($this->parent) ? $this->parent->getName() : null,
'config' => $this->bp->getName(),
'showCompact' => true,
'_disableLayout' => true
]))
->on(TermInput::ON_ENRICH, $termValidator)
->on(TermInput::ON_ADD, $termValidator)
->on(TermInput::ON_PASTE, $termValidator)
->on(TermInput::ON_SAVE, $termValidator)
);
}
protected function assembleHostElements(): void
{
if ($this->bp->getBackend() instanceof MonitoringBackend) {
$suggestionsPath = 'businessprocess/suggestions/monitoring-host';
} else {
$suggestionsPath = 'businessprocess/suggestions/icingadb-host';
}
return $list;
}
$this->addElement($this->createChildrenElementForObjects(
$this->translate('Hosts'),
$suggestionsPath
));
protected function hasMoreConfigs()
{
$configs = $this->enumConfigs();
return !empty($configs);
}
protected function enumConfigs()
{
return $this->storage->listProcesses();
}
/**
* Collect the given node's parents recursively into the given array by their names
*
* @param BpNode $node
* @param BpNode[] $parents
*/
protected function collectAllParents(BpNode $node, array &$parents)
{
foreach ($node->getParents() as $parent) {
$parents[$parent->getName()] = $parent;
$this->collectAllParents($parent, $parents);
$this->addElement('checkbox', 'host_override', [
'ignore' => true,
'class' => 'autosubmit',
'label' => $this->translate('Override Host State')
]);
if ($this->getPopulatedValue('host_override') === 'y') {
$this->addElement(new IplStateOverrides('stateOverrides', [
'label' => $this->translate('State Overrides'),
'options' => [
0 => $this->translate('UP'),
1 => $this->translate('DOWN'),
99 => $this->translate('PENDING')
]
]));
}
}
public function onSuccess()
protected function assembleServiceElements(): void
{
if ($this->bp->getBackend() instanceof MonitoringBackend) {
$suggestionsPath = 'businessprocess/suggestions/monitoring-service';
} else {
$suggestionsPath = 'businessprocess/suggestions/icingadb-service';
}
$this->addElement($this->createChildrenElementForObjects(
$this->translate('Services'),
$suggestionsPath
));
$this->addElement('checkbox', 'service_override', [
'ignore' => true,
'class' => 'autosubmit',
'label' => $this->translate('Override Service State')
]);
if ($this->getPopulatedValue('service_override') === 'y') {
$this->addElement(new IplStateOverrides('stateOverrides', [
'label' => $this->translate('State Overrides'),
'options' => [
0 => $this->translate('OK'),
1 => $this->translate('WARNING'),
2 => $this->translate('CRITICAL'),
3 => $this->translate('UNKNOWN'),
99 => $this->translate('PENDING'),
]
]));
}
}
protected function createChildrenElementForObjects(string $label, string $suggestionsPath): TermInput
{
$termValidator = function (array $terms) {
(new HostServiceTermValidator())
->setParent($this->parent)
->isValid($terms);
};
return (new TermInput('children'))
->setRequired()
->setLabel($label)
->setVerticalTermDirection()
->setSuggestionUrl(Url::fromPath($suggestionsPath, [
'node' => isset($this->parent) ? $this->parent->getName() : null,
'config' => $this->bp->getName(),
'showCompact' => true,
'_disableLayout' => true
]))
->on(TermInput::ON_ENRICH, $termValidator)
->on(TermInput::ON_ADD, $termValidator)
->on(TermInput::ON_PASTE, $termValidator)
->on(TermInput::ON_SAVE, $termValidator);
}
protected function onSuccess()
{
$changes = ProcessChanges::construct($this->bp, $this->session);
switch ($this->getValue('node_type')) {
case 'host':
case 'service':
$nodeType = $this->getValue('node_type');
if (! $nodeType || $nodeType === 'new-process') {
$properties = $this->getValues();
if (! $properties['alias']) {
unset($properties['alias']);
}
if ($this->parent !== null) {
$properties['parentName'] = $this->parent->getName();
}
$changes->createNode(BpConfig::escapeName($this->getValue('name')), $properties);
} else {
$children = array_unique(array_map(function ($term) {
return $term->getSearchValue();
}, $this->getElement('children')->getTerms()));
if ($nodeType === 'host' || $nodeType === 'service') {
$stateOverrides = $this->getValue('stateOverrides');
if (! empty($stateOverrides)) {
$childOverrides = [];
foreach ($this->getValue('children') as $service) {
$childOverrides[$service] = $stateOverrides;
foreach ($children as $nodeName) {
$childOverrides[$nodeName] = $stateOverrides;
}
$changes->modifyNode($this->parent, [
'stateOverrides' => array_merge($this->parent->getStateOverrides(), $childOverrides)
]);
}
}
// Fallthrough
case 'process':
case 'hosts_from_filter':
case 'services_from_filter':
if ($this->hasParentNode()) {
$changes->addChildrenToNode($this->getValue('children'), $this->parent);
} else {
foreach ($this->getValue('children') as $nodeName) {
$changes->copyNode($nodeName);
}
if ($this->parent !== null) {
$changes->addChildrenToNode($children, $this->parent);
} else {
foreach ($children as $nodeName) {
$changes->copyNode($nodeName);
}
break;
case 'new-process':
$properties = $this->getValues();
unset($properties['name']);
if (! $properties['alias']) {
unset($properties['alias']);
}
if ($this->hasParentNode()) {
$properties['parentName'] = $this->parent->getName();
}
$changes->createNode(BpConfig::escapeName($this->getValue('name')), $properties);
break;
}
}
// Trigger session destruction to make sure it get's stored.
// TODO: figure out why this is necessary, might be an unclean shutdown on redirect
unset($changes);
parent::onSuccess();
}
}

View file

@ -4,397 +4,312 @@ namespace Icinga\Module\Businessprocess\Forms;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\Common\EnumList;
use Icinga\Module\Businessprocess\Modification\ProcessChanges;
use Icinga\Module\Businessprocess\Node;
use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
use Icinga\Module\Businessprocess\Web\Form\Validator\NoDuplicateChildrenValidator;
use Icinga\Module\Businessprocess\ServiceNode;
use Icinga\Module\Businessprocess\Web\Form\Element\IplStateOverrides;
use Icinga\Module\Businessprocess\Web\Form\Validator\HostServiceTermValidator;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Web\Session\SessionNamespace;
use ipl\Html\Attributes;
use ipl\Html\FormattedString;
use ipl\Html\HtmlElement;
use ipl\Html\ValidHtml;
use ipl\I18n\Translation;
use ipl\Web\Compat\CompatForm;
use ipl\Web\FormElement\TermInput\ValidatedTerm;
use ipl\Web\Url;
class EditNodeForm extends BpConfigBaseForm
class EditNodeForm extends CompatForm
{
use EnumList;
use Translation;
/** @var Node */
/** @var ?BpConfig */
protected $bp;
/** @var ?Node */
protected $node;
/** @var BpNode */
/** @var ?BpNode */
protected $parent;
protected $objectList = array();
protected $processList = array();
protected $service;
protected $host;
public function setup()
{
[$this->host, $suffix] = BpConfig::splitNodeName($this->getNode()->getName());
if ($suffix !== 'Hoststatus') {
$this->service = $suffix;
}
$view = $this->getView();
$nodeName = $this->getNode()->getAlias() ?? $this->getNode()->getName();
$this->addHtml(
'<h2>' . $view->escape(
sprintf($this->translate('Modify "%s"'), $nodeName)
) . '</h2>'
);
$monitoredNodeType = null;
if ($this->isService()) {
$monitoredNodeType = 'service';
} else {
$monitoredNodeType = 'host';
}
$type = $this->selectNodeType($monitoredNodeType);
switch ($type) {
case 'host':
$this->selectHost();
break;
case 'service':
$this->selectService();
break;
case 'process':
$this->selectProcess();
break;
case 'new-process':
$this->addNewProcess();
break;
case null:
$this->setSubmitLabel($this->translate('Next'));
return;
}
}
protected function isService()
{
if (strpos($this->getNode()->getName(), ';Hoststatus')) {
return false;
}
return true;
}
protected function addNewProcess()
{
$this->addElement('text', 'name', array(
'label' => $this->translate('ID'),
'required' => true,
'disabled' => true,
'description' => $this->translate(
'This is the unique identifier of this process'
),
));
$this->addElement('text', 'alias', array(
'label' => $this->translate('Display Name'),
'description' => $this->translate(
'Usually this name will be shown for this node. Equals ID'
. ' if not given'
),
));
$this->addElement('select', 'operator', array(
'label' => $this->translate('Operator'),
'required' => true,
'multiOptions' => Node::getOperators()
));
$display = $this->getNode()->getDisplay() ?: 1;
$this->addElement('select', 'display', array(
'label' => $this->translate('Visualization'),
'required' => true,
'description' => $this->translate(
'Where to show this process'
),
'value' => $display,
'multiOptions' => array(
"$display" => $this->translate('Toplevel Process'),
'0' => $this->translate('Subprocess only'),
)
));
$this->addElement('text', 'infoUrl', array(
'label' => $this->translate('Info URL'),
'description' => $this->translate(
'URL pointing to more information about this node'
)
));
}
/** @var SessionNamespace */
protected $session;
/**
* @return string|null
*/
protected function selectNodeType($monitoredNodeType = null)
{
if ($this->hasParentNode()) {
$this->addElement('hidden', 'node_type', [
'disabled' => true,
'decorators' => ['ViewHelper'],
'value' => $monitoredNodeType
]);
return $monitoredNodeType;
} elseif (! $this->hasProcesses()) {
$this->addElement('hidden', 'node_type', array(
'ignore' => true,
'decorators' => array('ViewHelper'),
'value' => 'new-process'
));
return 'new-process';
}
}
protected function selectHost()
{
$this->addElement('select', 'children', array(
'required' => true,
'value' => $this->getNode()->getName(),
'multiOptions' => $this->enumHostList(),
'label' => $this->translate('Host'),
'description' => $this->translate('The host for this business process node'),
'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]]
));
$this->addHostOverrideCheckbox();
$hostOverrideSent = $this->getSentValue('host_override');
if ($hostOverrideSent === '1'
|| ($hostOverrideSent === null && $this->getElement('host_override')->isChecked())
) {
$this->addHostOverrideElement();
}
}
protected function selectService()
{
$this->addHostElement();
if ($this->getSentValue('hosts') === null) {
$this->addServicesElement($this->host);
$this->addServiceOverrideCheckbox();
if ($this->getElement('service_override')->isChecked() || $this->getSentValue('service_override') === '1') {
$this->addServiceOverrideElement();
}
} elseif ($host = $this->getSentValue('hosts')) {
$this->addServicesElement($host);
$this->addServiceOverrideCheckbox();
if ($this->getSentValue('service_override') === '1') {
$this->addServiceOverrideElement();
}
} else {
$this->setSubmitLabel($this->translate('Next'));
}
}
protected function addHostElement()
{
$this->addElement('select', 'hosts', array(
'label' => $this->translate('Host'),
'required' => true,
'ignore' => true,
'class' => 'autosubmit',
'multiOptions' => $this->optionalEnum($this->enumHostForServiceList()),
));
$this->getElement('hosts')->setValue($this->host);
}
protected function addHostOverrideCheckbox()
{
$this->addElement('checkbox', 'host_override', [
'ignore' => true,
'class' => 'autosubmit',
'value' => ! empty($this->parent->getStateOverrides($this->node->getName())),
'label' => $this->translate('Override Host State'),
'description' => $this->translate('Enable host state overrides')
]);
}
protected function addHostOverrideElement()
{
$this->addElement('stateOverrides', 'stateOverrides', [
'required' => true,
'states' => $this->enumHostStateList(),
'value' => $this->parent->getStateOverrides($this->node->getName()),
'label' => $this->translate('State Overrides')
]);
}
protected function addServicesElement($host)
{
$this->addElement('select', 'children', array(
'required' => true,
'value' => $this->getNode()->getName(),
'multiOptions' => $this->enumServiceList($host),
'label' => $this->translate('Service'),
'description' => $this->translate('The service for this business process node'),
'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]]
));
}
protected function addServiceOverrideCheckbox()
{
$this->addElement('checkbox', 'service_override', [
'ignore' => true,
'class' => 'autosubmit',
'value' => ! empty($this->parent->getStateOverrides($this->node->getName())),
'label' => $this->translate('Override Service State'),
'description' => $this->translate('Enable service state overrides')
]);
}
protected function addServiceOverrideElement()
{
$this->addElement('stateOverrides', 'stateOverrides', [
'required' => true,
'states' => $this->enumServiceStateList(),
'value' => $this->parent->getStateOverrides($this->node->getName()),
'label' => $this->translate('State Overrides')
]);
}
protected function selectProcess()
{
$this->addElement('multiselect', 'children', array(
'label' => $this->translate('Process nodes'),
'required' => true,
'size' => 8,
'multiOptions' => $this->enumProcesses(),
'description' => $this->translate(
'Other processes that should be part of this business process node'
)
));
}
/**
* @param BpNode|null $node
* Set the affected configuration
*
* @param BpConfig $bp
*
* @return $this
*/
public function setParentNode(BpNode $node = null)
public function setProcess(BpConfig $bp): self
{
$this->parent = $node;
$this->bp = $bp;
return $this;
}
/**
* @return bool
*/
public function hasParentNode()
{
return $this->parent !== null;
}
protected function hasProcesses()
{
return count($this->enumProcesses()) > 0;
}
protected function enumProcesses()
{
$list = array();
$parents = array();
if ($this->hasParentNode()) {
$this->collectAllParents($this->parent, $parents);
$parents[$this->parent->getName()] = $this->parent;
}
foreach ($this->bp->getNodes() as $node) {
if ($node instanceof BpNode && ! isset($parents[$node->getName()])) {
$list[$node->getName()] = $node->getName(); // display name?
}
}
return $list;
}
/**
* Collect the given node's parents recursively into the given array by their names
* Set the affected node
*
* @param BpNode $node
* @param BpNode[] $parents
*/
protected function collectAllParents(BpNode $node, array &$parents)
{
foreach ($node->getParents() as $parent) {
$parents[$parent->getName()] = $parent;
$this->collectAllParents($parent, $parents);
}
}
/**
* @param Node $node
*
* @return $this
*/
public function setNode(Node $node)
public function setNode(Node $node): self
{
$this->node = $node;
$this->populate([
'node-search' => $node->getName(),
'node-label' => $node->getAlias()
]);
return $this;
}
public function getNode()
/**
* Set the affected sub-process
*
* @param ?BpNode $node
*
* @return $this
*/
public function setParentNode(BpNode $node = null): self
{
return $this->node;
$this->parent = $node;
if ($this->node !== null) {
$stateOverrides = $this->parent->getStateOverrides($this->node->getName());
if (! empty($stateOverrides)) {
$this->populate([
'overrideStates' => 'y',
'stateOverrides' => $stateOverrides
]);
}
}
return $this;
}
public function onSuccess()
/**
* Set the user's session
*
* @param SessionNamespace $session
*
* @return $this
*/
public function setSession(SessionNamespace $session): self
{
$this->session = $session;
return $this;
}
/**
* Identify and return the node the user has chosen
*
* @return Node
*/
protected function identifyChosenNode(): Node
{
$userInput = $this->getPopulatedValue('node');
$nodeName = $this->getPopulatedValue('node-search');
$nodeLabel = $this->getPopulatedValue('node-label');
if ($nodeName && $userInput === $nodeLabel) {
// User accepted a suggestion and didn't change it manually
$node = $this->bp->getNode($nodeName);
} elseif ($userInput && (! $nodeLabel || $userInput !== $nodeLabel)) {
// User didn't choose a suggestion or changed it manually
$node = $this->bp->getNode(BpConfig::joinNodeName($userInput, 'Hoststatus'));
} else {
// If the search and user input are both empty, it can only be the initial value
$node = $this->node;
}
return $node;
}
protected function assemble()
{
$this->addHtml(new HtmlElement('h2', null, FormattedString::create(
$this->translate('Modify "%s"'),
$this->node->getAlias() ?? $this->node->getName()
)));
if ($this->node instanceof ServiceNode) {
$this->assembleServiceElements();
} else {
$this->assembleHostElements();
}
$this->addElement('submit', 'btn_submit', [
'label' => $this->translate('Save Changes')
]);
}
protected function assembleServiceElements(): void
{
if ($this->bp->getBackend() instanceof MonitoringBackend) {
$suggestionsPath = 'businessprocess/suggestions/monitoring-service';
} else {
$suggestionsPath = 'businessprocess/suggestions/icingadb-service';
}
$node = $this->identifyChosenNode();
$this->addHtml($this->createSearchInput(
$this->translate('Service'),
$node->getAlias() ?? $node->getName(),
$suggestionsPath
));
$this->addElement('checkbox', 'overrideStates', [
'ignore' => true,
'class' => 'autosubmit',
'label' => $this->translate('Override Service State')
]);
if ($this->getPopulatedValue('overrideStates') === 'y') {
$this->addElement(new IplStateOverrides('stateOverrides', [
'label' => $this->translate('State Overrides'),
'options' => [
0 => $this->translate('OK'),
1 => $this->translate('WARNING'),
2 => $this->translate('CRITICAL'),
3 => $this->translate('UNKNOWN'),
99 => $this->translate('PENDING'),
]
]));
}
}
protected function assembleHostElements(): void
{
if ($this->bp->getBackend() instanceof MonitoringBackend) {
$suggestionsPath = 'businessprocess/suggestions/monitoring-host';
} else {
$suggestionsPath = 'businessprocess/suggestions/icingadb-host';
}
$node = $this->identifyChosenNode();
$this->addHtml($this->createSearchInput(
$this->translate('Host'),
$node->getAlias() ?? $node->getName(),
$suggestionsPath
));
$this->addElement('checkbox', 'overrideStates', [
'ignore' => true,
'class' => 'autosubmit',
'label' => $this->translate('Override Host State')
]);
if ($this->getPopulatedValue('overrideStates') === 'y') {
$this->addElement(new IplStateOverrides('stateOverrides', [
'label' => $this->translate('State Overrides'),
'options' => [
0 => $this->translate('UP'),
1 => $this->translate('DOWN'),
99 => $this->translate('PENDING')
]
]));
}
}
protected function createSearchInput(string $label, string $value, string $suggestionsPath): ValidHtml
{
$userInput = $this->createElement('text', 'node', [
'ignore' => true,
'required' => true,
'autocomplete' => 'off',
'label' => $label,
'value' => $value,
'data-enrichment-type' => 'completion',
'data-term-suggestions' => '#node-suggestions',
'data-suggest-url' => Url::fromPath($suggestionsPath, [
'node' => isset($this->parent) ? $this->parent->getName() : null,
'config' => $this->bp->getName(),
'showCompact' => true,
'_disableLayout' => true
]),
'validators' => ['callback' => function ($_, $validator) {
$newName = $this->identifyChosenNode()->getName();
if ($newName === $this->node->getName()) {
return true;
}
$term = new ValidatedTerm($newName);
(new HostServiceTermValidator())
->setParent($this->parent)
->isValid($term);
if (! $term->isValid()) {
$validator->addMessage($term->getMessage());
return false;
}
return true;
}]
]);
$fieldset = new HtmlElement('fieldset');
$searchInput = $this->createElement('hidden', 'node-search', ['ignore' => true]);
$this->registerElement($searchInput);
$fieldset->addHtml($searchInput);
$labelInput = $this->createElement('hidden', 'node-label', ['ignore' => true]);
$this->registerElement($labelInput);
$fieldset->addHtml($labelInput);
$this->registerElement($userInput);
$this->decorate($userInput);
$fieldset->addHtml(
$userInput,
new HtmlElement('div', Attributes::create([
'id' => 'node-suggestions',
'class' => 'search-suggestions'
]))
);
return $fieldset;
}
protected function onSuccess()
{
$changes = ProcessChanges::construct($this->bp, $this->session);
$children = $this->parent->getChildNames();
$previousPos = array_search($this->node->getName(), $children, true);
$node = $this->identifyChosenNode();
$nodeName = $node->getName();
$changes->deleteNode($this->node, $this->parent->getName());
$changes->addChildrenToNode([$nodeName], $this->parent);
switch ($this->getValue('node_type')) {
case 'host':
case 'service':
$stateOverrides = $this->getValue('stateOverrides') ?: [];
if (! empty($stateOverrides)) {
$stateOverrides = array_merge(
$this->parent->getStateOverrides(),
[$this->getValue('children') => $stateOverrides]
);
} else {
$stateOverrides = $this->parent->getStateOverrides();
unset($stateOverrides[$this->getValue('children')]);
}
$changes->modifyNode($this->parent, ['stateOverrides' => $stateOverrides]);
// Fallthrough
case 'process':
$changes->addChildrenToNode($this->getValue('children'), $this->parent);
break;
case 'new-process':
$properties = $this->getValues();
unset($properties['name']);
if ($this->hasParentNode()) {
$properties['parentName'] = $this->parent->getName();
}
$changes->createNode($this->getValue('name'), $properties);
break;
$stateOverrides = $this->getValue('stateOverrides');
if (! empty($stateOverrides)) {
$changes->modifyNode($this->parent, [
'stateOverrides' => array_merge($this->parent->getStateOverrides(), [
$nodeName => $stateOverrides
])
]);
}
if ($this->bp->getMetadata()->isManuallyOrdered() && ($newPos = count($children) - 1) > $previousPos) {
$changes->moveNode(
$node,
$newPos,
$previousPos,
$this->parent->getName(),
$this->parent->getName()
);
}
// Trigger session destruction to make sure it get's stored.
// TODO: figure out why this is necessary, might be an unclean shutdown on redirect
unset($changes);
parent::onSuccess();
}
public function isValid($data)
{
// Don't allow to override disabled elements. This is probably too harsh
// but also wouldn't be necessary if this would be a Icinga\Web\Form...
foreach ($this->getElements() as $element) {
/** @var \Zend_Form_Element $element */
if ($element->getAttrib('disabled')) {
$data[$element->getName()] = $element->getValue();
}
}
return parent::isValid($data);
}
}

View file

@ -173,6 +173,8 @@ class BpNode extends Node
if (! empty($this->children)) {
unset($this->children[$name]);
}
$this->childNames = array_values($this->childNames);
}
return $this;

View file

@ -1,170 +0,0 @@
<?php
namespace Icinga\Module\Businessprocess\Common;
use Icinga\Application\Modules\Module;
use Icinga\Data\Filter\Filter;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\IcingaDbObject;
use Icinga\Module\Businessprocess\MonitoringRestrictions;
use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
trait EnumList
{
protected function enumHostForServiceList()
{
if ($this->useIcingaDbBackend()) {
$names = (new IcingaDbObject())->yieldHostnames();
} else {
$names = $this->backend
->select()
->from('hostStatus', ['hostname' => 'host_name'])
->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
->order('host_name')
->getQuery()
->fetchColumn();
}
// fetchPairs doesn't seem to work when using the same column with
// different aliases twice
$res = array();
foreach ($names as $name) {
$res[$name] = $name;
}
return $res;
}
protected function enumHostList()
{
if ($this->useIcingaDbBackend()) {
$names = (new IcingaDbObject())->yieldHostnames();
} else {
$names = $this->backend
->select()
->from('hostStatus', ['hostname' => 'host_name'])
->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
->order('host_name')
->getQuery()
->fetchColumn();
}
// fetchPairs doesn't seem to work when using the same column with
// different aliases twice
$res = array();
foreach ($names as $name) {
$res[BpConfig::joinNodeName($name, 'Hoststatus')] = $name;
}
return $res;
}
protected function enumServiceList($host)
{
if ($this->useIcingaDbBackend()) {
$names = (new IcingaDbObject())->yieldServicenames($host);
} else {
$names = $this->backend
->select()
->from('serviceStatus', ['service' => 'service_description'])
->where('host_name', $host)
->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
->order('service_description')
->getQuery()
->fetchColumn();
}
$services = array();
foreach ($names as $name) {
$services[BpConfig::joinNodeName($host, $name)] = $name;
}
return $services;
}
protected function enumHostListByFilter($filter)
{
if ($this->useIcingaDbBackend()) {
$names = (new IcingaDbObject())->yieldHostnames($filter);
} else {
$names = $this->backend
->select()
->from('hostStatus', ['hostname' => 'host_name'])
->applyFilter(Filter::fromQueryString($filter))
->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
->order('host_name')
->getQuery()
->fetchColumn();
}
// fetchPairs doesn't seem to work when using the same column with
// different aliases twice
$res = array();
foreach ($names as $name) {
$res[BpConfig::joinNodeName($name, 'Hoststatus')] = $name;
}
return $res;
}
protected function enumServiceListByFilter($filter)
{
$services = array();
if ($this->useIcingaDbBackend()) {
$objects = (new IcingaDbObject())->fetchServices($filter);
foreach ($objects as $object) {
$services[BpConfig::joinNodeName($object->host->name, $object->name)]
= $object->host->name . ':' . $object->name;
}
} else {
$objects = $this->backend
->select()
->from('serviceStatus', ['host' => 'host_name', 'service' => 'service_description'])
->applyFilter(Filter::fromQueryString($filter))
->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
->order('service_description')
->getQuery()
->fetchAll();
foreach ($objects as $object) {
$services[BpConfig::joinNodeName($object->host, $object->service)]
= $object->host . ':' . $object->service;
}
}
return $services;
}
protected function enumHostStateList()
{
$hostStateList = [
0 => $this->translate('UP'),
1 => $this->translate('DOWN'),
99 => $this->translate('PENDING')
];
return $hostStateList;
}
protected function enumServiceStateList()
{
$serviceStateList = [
0 => $this->translate('OK'),
1 => $this->translate('WARNING'),
2 => $this->translate('CRITICAL'),
3 => $this->translate('UNKNOWN'),
99 => $this->translate('PENDING'),
];
return $serviceStateList;
}
protected function useIcingaDbBackend()
{
if (Module::exists('icingadb')) {
return ! $this->bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend();
}
return false;
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Icinga\Module\Businessprocess\Monitoring\Backend\Ido\Query;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServicecommenthistoryQuery;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServicecommentQuery;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServicedowntimeQuery;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServicedowntimestarthistoryQuery;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServiceflappingstarthistoryQuery;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServicegroupQuery;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServicenotificationQuery;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServicestatehistoryQuery;
use Zend_Db_Select;
trait CustomVarJoinTemplateOverride
{
private $customVarsJoinTemplate = '%1$s = %2$s.object_id AND %2$s.varname LIKE %3$s';
/**
* This is a 1:1 copy of {@see IdoQuery::joinCustomvar()} to be able to
* adjust {@see IdoQuery::$customVarsJoinTemplate} as it's private
*/
protected function joinCustomvar($customvar)
{
// TODO: This is not generic enough yet
list($type, $name) = $this->customvarNameToTypeName($customvar);
$alias = ($type === 'host' ? 'hcv_' : 'scv_') . preg_replace('~[^a-zA-Z0-9_]~', '_', $name);
// We're replacing any problematic char with an underscore, which will lead to duplicates, this avoids them
$from = $this->select->getPart(Zend_Db_Select::FROM);
for ($i = 2; array_key_exists($alias, $from); $i++) {
$alias = $alias . '_' . $i;
}
$this->customVars[strtolower($customvar)] = $alias;
if ($type === 'host') {
if ($this instanceof ServicecommentQuery
|| $this instanceof ServicedowntimeQuery
|| $this instanceof ServicecommenthistoryQuery
|| $this instanceof ServicedowntimestarthistoryQuery
|| $this instanceof ServiceflappingstarthistoryQuery
|| $this instanceof ServicegroupQuery
|| $this instanceof ServicenotificationQuery
|| $this instanceof ServicestatehistoryQuery
|| $this instanceof \Icinga\Module\Monitoring\Backend\Ido\Query\ServicestatusQuery
) {
$this->requireVirtualTable('services');
$leftcol = 's.host_object_id';
} else {
$leftcol = 'ho.object_id';
if (! $this->hasJoinedTable('ho')) {
$this->requireVirtualTable('hosts');
}
}
} else { // $type === 'service'
$leftcol = 'so.object_id';
if (! $this->hasJoinedTable('so')) {
$this->requireVirtualTable('services');
}
}
$mapped = $this->getMappedField($leftcol);
if ($mapped !== null) {
$this->requireColumn($leftcol);
$leftcol = $mapped;
}
$joinOn = sprintf(
$this->customVarsJoinTemplate,
$leftcol,
$alias,
$this->db->quote($name)
);
$this->select->joinLeft(
array($alias => $this->prefix . 'customvariablestatus'),
$joinOn,
array()
);
return $this;
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace Icinga\Module\Businessprocess\Monitoring\Backend\Ido\Query;
class HostStatusQuery extends \Icinga\Module\Monitoring\Backend\Ido\Query\HoststatusQuery
{
use CustomVarJoinTemplateOverride;
}

View file

@ -0,0 +1,8 @@
<?php
namespace Icinga\Module\Businessprocess\Monitoring\Backend\Ido\Query;
class ServiceStatusQuery extends \Icinga\Module\Monitoring\Backend\Ido\Query\ServicestatusQuery
{
use CustomVarJoinTemplateOverride;
}

View file

@ -0,0 +1,16 @@
<?php
namespace Icinga\Module\Businessprocess\Monitoring\DataView;
use Icinga\Data\ConnectionInterface;
use Icinga\Module\Businessprocess\Monitoring\Backend\Ido\Query\HostStatusQuery;
class HostStatus extends \Icinga\Module\Monitoring\DataView\Hoststatus
{
public function __construct(ConnectionInterface $connection, array $columns = null)
{
parent::__construct($connection, $columns);
$this->query = new HostStatusQuery($connection->getResource(), $columns);
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Icinga\Module\Businessprocess\Monitoring\DataView;
use Icinga\Data\ConnectionInterface;
use Icinga\Module\Businessprocess\Monitoring\Backend\Ido\Query\ServiceStatusQuery;
class ServiceStatus extends \Icinga\Module\Monitoring\DataView\Servicestatus
{
public function __construct(ConnectionInterface $connection, array $columns = null)
{
parent::__construct($connection, $columns);
$this->query = new ServiceStatusQuery($connection->getResource(), $columns);
}
}

View file

@ -3,9 +3,12 @@
namespace Icinga\Module\Businessprocess;
use Icinga\Module\Businessprocess\Web\Url;
use ipl\I18n\Translation;
class ServiceNode extends MonitoredNode
{
use Translation;
protected $hostname;
/** @var string Alias of the host */
@ -69,7 +72,11 @@ class ServiceNode extends MonitoredNode
return null;
}
return $this->getHostAlias() . ': ' . $this->alias;
return sprintf(
$this->translate('%s on %s', '<service> on <host>'),
$this->alias,
$this->getHostAlias()
);
}
public function getUrl()

View file

@ -0,0 +1,75 @@
<?php
namespace Icinga\Module\Businessprocess\Web\Form\Element;
use ipl\Html\Attributes;
use ipl\Html\FormElement\FieldsetElement;
class IplStateOverrides extends FieldsetElement
{
/** @var array */
protected $options = [];
/**
* Set the options show
*
* @param array $options
*
* @return $this
*/
public function setOptions(array $options): self
{
$this->options = $options;
return $this;
}
/**
* Get the options to show
*
* @return array
*/
public function getOptions(): array
{
return $this->options;
}
public function getValues()
{
$cleanedValue = parent::getValues();
if (! empty($cleanedValue)) {
foreach ($cleanedValue as $from => $to) {
if ((int) $from === (int) $to) {
unset($cleanedValue[$from]);
}
}
}
return $cleanedValue;
}
protected function assemble()
{
$states = $this->getOptions();
foreach ($states as $state => $label) {
if ($state === 0) {
continue;
}
$this->addElement('select', $state, [
'label' => $label,
'value' => $state,
'options' => [$state => $this->translate('Keep actual state')] + $states
]);
}
}
protected function registerAttributeCallbacks(Attributes $attributes)
{
parent::registerAttributeCallbacks($attributes);
$this->getAttributes()
->registerAttributeCallback('options', null, [$this, 'setOptions']);
}
}

View file

@ -1,55 +0,0 @@
<?php
namespace Icinga\Module\Businessprocess\Web\Form\Element;
class StateOverrides extends FormElement
{
public $helper = 'formStateOverrides';
/** @var array The overridable states */
protected $states;
/**
* Set the overridable states
*
* @param array $states
*
* @return $this
*/
public function setStates(array $states)
{
$this->states = $states;
return $this;
}
/**
* Get the overridable states
*
* @return array
*/
public function getStates()
{
return $this->states;
}
public function init()
{
$this->setIsArray(true);
}
public function setValue($value)
{
$cleanedValue = [];
if (! empty($value)) {
foreach ($value as $from => $to) {
if ((int) $from !== (int) $to) {
$cleanedValue[$from] = $to;
}
}
}
return parent::setValue($cleanedValue);
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace Icinga\Module\Businessprocess\Web\Form\Validator;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\ServiceNode;
use Icinga\Module\Businessprocess\State\IcingaDbState;
use Icinga\Module\Businessprocess\State\MonitoringState;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use ipl\I18n\Translation;
use ipl\Validator\BaseValidator;
use ipl\Web\FormElement\TermInput\Term;
use LogicException;
class HostServiceTermValidator extends BaseValidator
{
use Translation;
/** @var ?BpNode */
protected $parent;
/**
* Set the affected process
*
* @param BpNode $parent
*
* @return $this
*/
public function setParent(BpNode $parent): self
{
$this->parent = $parent;
return $this;
}
public function isValid($terms)
{
if ($this->parent === null) {
throw new LogicException('Missing parent process. Cannot validate terms.');
}
if (! is_array($terms)) {
$terms = [$terms];
}
$testConfig = new BpConfig();
foreach ($terms as $term) {
/** @var Term $term */
[$hostName, $serviceName] = BpConfig::splitNodeName($term->getSearchValue());
if ($serviceName !== null && $serviceName !== 'Hoststatus') {
$node = $testConfig->createService($hostName, $serviceName);
} else {
$node = $testConfig->createHost($hostName);
if ($serviceName === null) {
$term->setSearchValue(BpConfig::joinNodeName($hostName, 'Hoststatus'));
}
}
if ($this->parent->hasChild($term->getSearchValue())) {
$term->setMessage($this->translate('Already defined in this process'));
} else {
$testConfig->getNode('__unbound__')
->addChild($node);
}
}
if ($this->parent->getBpConfig()->getBackend() instanceof MonitoringBackend) {
MonitoringState::apply($testConfig);
} else {
IcingaDbState::apply($testConfig);
}
foreach ($terms as $term) {
/** @var Term $term */
$node = $testConfig->getNode($term->getSearchValue());
if ($node->isMissing()) {
if ($node instanceof ServiceNode) {
$term->setMessage($this->translate('Service not found'));
} else {
$term->setMessage($this->translate('Host not found'));
}
} else {
$term->setLabel($node->getAlias());
$term->setClass($node->getObjectClassName());
}
}
}
}

View file

@ -1,57 +0,0 @@
<?php
namespace Icinga\Module\Businessprocess\Web\Form\Validator;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\Forms\EditNodeForm;
use Icinga\Module\Businessprocess\Web\Form\QuickForm;
use Zend_Validate_Abstract;
class NoDuplicateChildrenValidator extends Zend_Validate_Abstract
{
const CHILD_FOUND = 'childFound';
/** @var QuickForm */
protected $form;
/** @var BpConfig */
protected $bp;
/** @var BpNode */
protected $parent;
/** @var string */
protected $label;
public function __construct(QuickForm $form, BpConfig $bp, BpNode $parent = null)
{
$this->form = $form;
$this->bp = $bp;
$this->parent = $parent;
$this->_messageVariables['label'] = 'label';
$this->_messageTemplates = [
self::CHILD_FOUND => mt('businessprocess', '%label% is already defined in this process')
];
}
public function isValid($value)
{
if ($this->parent === null) {
$found = $this->bp->hasRootNode($value);
} elseif ($this->form instanceof EditNodeForm && $this->form->getNode()->getName() === $value) {
$found = false;
} else {
$found = $this->parent->hasChild($value);
}
if (! $found) {
return true;
}
$this->label = $this->form->getElement('children')->getMultiOptions()[$value];
$this->_error(self::CHILD_FOUND);
return false;
}
}

View file

@ -869,29 +869,6 @@ form.bp-form {
}
}
#stateOverrides-element {
display: inline-table;
table-layout: fixed;
border-spacing: .5em;
padding: 0;
label {
display: table-row;
span, select {
display: table-cell;
}
span {
width: 10em;
}
select {
width: 26em;
}
}
}
fieldset {
min-width: 36em;
}

View file

@ -27,7 +27,7 @@ class ServiceNodeTest extends BaseTestCase
public function testReturnsCorrectAlias()
{
$this->assertEquals(
'localhost: ping <> pong',
'ping <> pong on localhost',
$this->pingOnLocalhost()->getAlias()
);
}
@ -36,7 +36,7 @@ class ServiceNodeTest extends BaseTestCase
{
$this->assertEquals(
'<a href="/icingaweb2/businessprocess/service/show?host=localhost&amp;service=ping%20%3C%3E%20pong">'
. 'localhost: ping &lt;&gt; pong</a>',
. 'ping &lt;&gt; pong on localhost</a>',
$this->pingOnLocalhost()->getLink()->render()
);
}