summaryrefslogtreecommitdiffstats
path: root/application/controllers
diff options
context:
space:
mode:
Diffstat (limited to 'application/controllers')
-rw-r--r--application/controllers/HostController.php66
-rw-r--r--application/controllers/IndexController.php20
-rw-r--r--application/controllers/NodeController.php148
-rw-r--r--application/controllers/ProcessController.php780
-rw-r--r--application/controllers/ServiceController.php74
-rw-r--r--application/controllers/SuggestionsController.php372
6 files changed, 1460 insertions, 0 deletions
diff --git a/application/controllers/HostController.php b/application/controllers/HostController.php
new file mode 100644
index 0000000..e22edde
--- /dev/null
+++ b/application/controllers/HostController.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Controllers;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Businessprocess\IcingaDbObject;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\DataView\DataView;
+use Icinga\Web\Url;
+use ipl\Stdlib\Filter;
+
+class HostController extends Controller
+{
+ /**
+ * True if business process prefers to use icingadb as backend for it's nodes
+ *
+ * @var bool
+ */
+ protected $isIcingadbPreferred;
+
+ protected function moduleInit()
+ {
+ $this->isIcingadbPreferred = Module::exists('icingadb')
+ && ! $this->params->has('backend')
+ && IcingadbSupport::useIcingaDbAsBackend();
+
+ if (! $this->isIcingadbPreferred) {
+ parent::moduleInit();
+ }
+ }
+
+ public function showAction()
+ {
+ if ($this->isIcingadbPreferred) {
+ $hostName = $this->params->shift('host');
+
+ $query = Host::on(IcingaDbObject::fetchDb());
+ IcingaDbObject::applyIcingaDbRestrictions($query);
+
+ $query->filter(Filter::equal('host.name', $hostName));
+
+ $host = $query->first();
+
+ $this->params->add('name', $hostName);
+
+ if ($host !== null) {
+ $this->redirectNow(Url::fromPath('icingadb/host')->setParams($this->params));
+ }
+ } else {
+ $hostName = $this->params->get('host');
+
+ $query = $this->backend->select()
+ ->from('hoststatus', array('host_name'))
+ ->where('host_name', $hostName);
+
+ $this->applyRestriction('monitoring/filter/objects', $query);
+ if ($query->fetchRow() !== false) {
+ $this->redirectNow(Url::fromPath('monitoring/host/show')->setParams($this->params));
+ }
+ }
+
+ $this->view->host = $hostName;
+ }
+}
diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php
new file mode 100644
index 0000000..60ddc70
--- /dev/null
+++ b/application/controllers/IndexController.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Controllers;
+
+use Icinga\Module\Businessprocess\Web\Controller;
+use Icinga\Module\Businessprocess\Web\Component\Dashboard;
+
+class IndexController extends Controller
+{
+ /**
+ * Show an overview page
+ */
+ public function indexAction()
+ {
+ $this->setTitle($this->translate('Business Process Overview'));
+ $this->controls()->add($this->overviewTab());
+ $this->content()->add(Dashboard::create($this->Auth(), $this->storage()));
+ $this->setAutorefreshInterval(15);
+ }
+}
diff --git a/application/controllers/NodeController.php b/application/controllers/NodeController.php
new file mode 100644
index 0000000..e5c657f
--- /dev/null
+++ b/application/controllers/NodeController.php
@@ -0,0 +1,148 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Controllers;
+
+use Exception;
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Businessprocess\Renderer\Breadcrumb;
+use Icinga\Module\Businessprocess\Renderer\TileRenderer;
+use Icinga\Module\Businessprocess\Simulation;
+use Icinga\Module\Businessprocess\State\IcingaDbState;
+use Icinga\Module\Businessprocess\State\MonitoringState;
+use Icinga\Module\Businessprocess\Web\Controller;
+use Icinga\Module\Businessprocess\Web\Url;
+use ipl\Html\Html;
+use ipl\Web\Widget\Link;
+
+class NodeController extends Controller
+{
+ public function impactAction()
+ {
+ $this->setAutorefreshInterval(10);
+ $content = $this->content();
+ $this->controls()->add(
+ $this->singleTab($this->translate('Node Impact'))
+ );
+ $name = $this->params->get('name');
+ $this->addTitle($this->translate('Business Impact (%s)'), $name);
+
+ $brokenFiles = [];
+ $simulation = Simulation::fromSession($this->session());
+ foreach ($this->storage()->listProcessNames() as $configName) {
+ try {
+ $config = $this->storage()->loadProcess($configName);
+ } catch (Exception $e) {
+ $meta = $this->storage()->loadMetadata($configName);
+ $brokenFiles[$meta->get('Title')] = $configName;
+ continue;
+ }
+
+ $parents = [];
+ if ($config->hasNode($name)) {
+ foreach ($config->getNode($name)->getPaths() as $path) {
+ array_pop($path); // Remove the monitored node
+ $immediateParentName = array_pop($path); // The directly affected process
+ $parents[] = [$config->getNode($immediateParentName), $path];
+ }
+ }
+
+ $askedConfigs = [];
+ foreach ($config->getImportedNodes() as $importedNode) {
+ $importedConfig = $importedNode->getBpConfig();
+
+ if (isset($askedConfigs[$importedConfig->getName()])) {
+ continue;
+ } else {
+ $askedConfigs[$importedConfig->getName()] = true;
+ }
+
+ if ($importedConfig->hasNode($name)) {
+ $node = $importedConfig->getNode($name);
+ $nativePaths = $node->getPaths($config);
+
+ do {
+ $path = array_pop($nativePaths);
+ $importedNodePos = array_search($importedNode->getIdentifier(), $path, true);
+ if ($importedNodePos !== false) {
+ array_pop($path); // Remove the monitored node
+ $immediateParentName = array_pop($path); // The directly affected process
+ $importedPath = array_slice($path, $importedNodePos + 1);
+
+ // We may get multiple native paths. Though, only the right hand of the path
+ // is what we're interested in. The left part is not what is getting imported.
+ $antiDuplicator = join('|', $importedPath) . '|' . $immediateParentName;
+ if (isset($parents[$antiDuplicator])) {
+ continue;
+ }
+
+ foreach ($importedNode->getPaths($config) as $targetPath) {
+ if ($targetPath[count($targetPath) - 1] === $immediateParentName) {
+ array_pop($targetPath);
+ $parent = $importedNode;
+ } else {
+ $parent = $importedConfig->getNode($immediateParentName);
+ }
+
+ $parents[$antiDuplicator] = [$parent, array_merge($targetPath, $importedPath)];
+ }
+ }
+ } while (! empty($nativePaths));
+ }
+ }
+
+ if (empty($parents)) {
+ continue;
+ }
+
+ if (Module::exists('icingadb') &&
+ (! $config->getBackendName() && IcingadbSupport::useIcingaDbAsBackend())
+ ) {
+ IcingaDbState::apply($config);
+ } else {
+ MonitoringState::apply($config);
+ }
+ $config->applySimulation($simulation);
+
+ foreach ($parents as $parentAndPath) {
+ $renderer = (new TileRenderer($config, array_shift($parentAndPath)))
+ ->setUrl(Url::fromPath('businessprocess/process/show', ['config' => $configName]))
+ ->setPath(array_shift($parentAndPath));
+
+ $bc = Breadcrumb::create($renderer);
+ $bc->getAttributes()->set('data-base-target', '_next');
+ $content->add($bc);
+ }
+ }
+
+ if ($content->isEmpty()) {
+ $content->add($this->translate('No impact detected. Is this node part of a business process?'));
+ }
+
+ if (! empty($brokenFiles)) {
+ $elem = Html::tag(
+ 'ul',
+ ['class' => 'broken-files'],
+ tp(
+ 'The following business process has an invalid config file and therefore cannot be read:',
+ 'The following business processes have invalid config files and therefore cannot be read:',
+ count($brokenFiles)
+ )
+ );
+
+ foreach ($brokenFiles as $bpName => $fileName) {
+ $elem->addHtml(
+ Html::tag(
+ 'li',
+ new Link(
+ sprintf('%s (%s.conf)', $bpName, $fileName),
+ \ipl\Web\Url::fromPath('businessprocess/process/show', ['config' => $fileName])
+ )
+ )
+ );
+ }
+
+ $content->addHtml($elem);
+ }
+ }
+}
diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php
new file mode 100644
index 0000000..208c91e
--- /dev/null
+++ b/application/controllers/ProcessController.php
@@ -0,0 +1,780 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Controllers;
+
+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;
+use Icinga\Module\Businessprocess\Renderer\Renderer;
+use Icinga\Module\Businessprocess\Renderer\TileRenderer;
+use Icinga\Module\Businessprocess\Renderer\TreeRenderer;
+use Icinga\Module\Businessprocess\Simulation;
+use Icinga\Module\Businessprocess\State\IcingaDbState;
+use Icinga\Module\Businessprocess\State\MonitoringState;
+use Icinga\Module\Businessprocess\Storage\ConfigDiff;
+use Icinga\Module\Businessprocess\Storage\LegacyConfigRenderer;
+use Icinga\Module\Businessprocess\Web\Component\ActionBar;
+use Icinga\Module\Businessprocess\Web\Component\RenderedProcessActionBar;
+use Icinga\Module\Businessprocess\Web\Component\Tabs;
+use Icinga\Module\Businessprocess\Web\Controller;
+use Icinga\Util\Json;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+use ipl\Html\Form;
+use ipl\Html\Html;
+use ipl\Html\HtmlElement;
+use ipl\Html\HtmlString;
+use ipl\Html\TemplateString;
+use ipl\Html\Text;
+use ipl\Web\Control\SortControl;
+use ipl\Web\FormElement\TermInput;
+use ipl\Web\Widget\Link;
+use ipl\Web\Widget\Icon;
+
+class ProcessController extends Controller
+{
+ /** @var Renderer */
+ protected $renderer;
+
+ /**
+ * Create a new Business Process Configuration
+ */
+ public function createAction()
+ {
+ $this->assertPermission('businessprocess/create');
+
+ $title = $this->translate('Create a new Business Process');
+ $this->setTitle($title);
+ $this->controls()
+ ->add($this->tabsForCreate()->activate('create'))
+ ->add(Html::tag('h1', null, $title));
+
+ $this->content()->add(
+ $this->loadForm('bpConfig')
+ ->setStorage($this->storage())
+ ->setSuccessUrl('businessprocess/process/show')
+ ->handleRequest()
+ );
+ }
+
+ /**
+ * Upload an existing Business Process Configuration
+ */
+ public function uploadAction()
+ {
+ $this->assertPermission('businessprocess/create');
+
+ $title = $this->translate('Upload a Business Process Config file');
+ $this->setTitle($title);
+ $this->controls()
+ ->add($this->tabsForCreate()->activate('upload'))
+ ->add(Html::tag('h1', null, $title));
+
+ $this->content()->add(
+ $this->loadForm('BpUpload')
+ ->setStorage($this->storage())
+ ->setSuccessUrl('businessprocess/process/show')
+ ->handleRequest()
+ );
+ }
+
+ /**
+ * Show a business process
+ */
+ public function showAction()
+ {
+ $bp = $this->loadModifiedBpConfig();
+ $node = $this->getNode($bp);
+
+ if (Module::exists('icingadb') &&
+ (! $bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend())
+ ) {
+ IcingaDbState::apply($bp);
+ } else {
+ MonitoringState::apply($bp);
+ }
+
+ $this->handleSimulations($bp);
+
+ $this->setTitle($this->translate('Business Process "%s"'), $bp->getTitle());
+
+ $renderer = $this->prepareRenderer($bp, $node);
+
+ if (! $this->showFullscreen && ($node === null || ! $renderer->rendersImportedNode())) {
+ if ($this->params->get('unlocked')) {
+ $renderer->unlock();
+ }
+
+ if ($bp->isEmpty() && $renderer->isLocked()) {
+ $this->redirectNow($this->url()->with('unlocked', true));
+ }
+ }
+
+ $this->handleFormatRequest($bp, $node);
+
+ $this->prepareControls($bp, $renderer);
+
+ $this->tabs()->extend(new OutputFormat());
+
+ $this->content()->add($this->showHints($bp, $renderer));
+ $this->content()->add($this->showWarnings($bp));
+ $this->content()->add($this->showErrors($bp));
+ $this->content()->add($renderer);
+ $this->loadActionForm($bp, $node);
+ $this->setDynamicAutorefresh();
+ }
+
+ /**
+ * Create a sort control and apply its sort specification to the given renderer
+ *
+ * @param Renderer $renderer
+ * @param BpConfig $config
+ *
+ * @return SortControl
+ */
+ protected function createBpSortControl(Renderer $renderer, BpConfig $config): SortControl
+ {
+ $defaultSort = $this->session()->get('sort.default', $renderer->getDefaultSort());
+ $options = [
+ 'display_name asc' => $this->translate('Name'),
+ 'state desc' => $this->translate('State')
+ ];
+ if ($config->getMetadata()->isManuallyOrdered()) {
+ $options['manual asc'] = $this->translate('Manual');
+ } elseif ($defaultSort === 'manual desc') {
+ $defaultSort = $renderer->getDefaultSort();
+ }
+
+ $sortControl = SortControl::create($options)
+ ->setDefault($defaultSort)
+ ->setMethod('POST')
+ ->setAttribute('name', 'bp-sort-control')
+ ->on(Form::ON_SUCCESS, function (SortControl $sortControl) use ($renderer) {
+ $sort = $sortControl->getSort();
+ if ($sort === $renderer->getDefaultSort()) {
+ $this->session()->delete('sort.default');
+ $url = Url::fromRequest()->without($sortControl->getSortParam());
+ } else {
+ $this->session()->set('sort.default', $sort);
+ $url = Url::fromRequest()->with($sortControl->getSortParam(), $sort);
+ }
+
+ $this->redirectNow($url);
+ })->handleRequest($this->getServerRequest());
+
+ $renderer->setSort($sortControl->getSort());
+ $this->params->shift($sortControl->getSortParam());
+
+ return $sortControl;
+ }
+
+ protected function prepareControls($bp, $renderer)
+ {
+ $controls = $this->controls();
+
+ if ($this->showFullscreen) {
+ $controls->getAttributes()->add('class', 'want-fullscreen');
+ $controls->add(Html::tag(
+ 'a',
+ [
+ 'href' => $this->url()->without('showFullscreen')->without('view'),
+ 'title' => $this->translate('Leave full screen and switch back to normal mode')
+ ],
+ new Icon('down-left-and-up-right-to-center')
+ ));
+ }
+
+ if (! ($this->showFullscreen || $this->view->compact)) {
+ $controls->add($this->getProcessTabs($bp, $renderer));
+ $controls->getAttributes()->add('class', 'separated');
+ }
+
+ $controls->add(Breadcrumb::create(clone $renderer));
+ if (! $this->showFullscreen && ! $this->view->compact) {
+ $controls->add(
+ new RenderedProcessActionBar($bp, $renderer, $this->url())
+ );
+ }
+
+ $controls->addHtml($this->createBpSortControl($renderer, $bp));
+ }
+
+ protected function getNode(BpConfig $bp)
+ {
+ if ($nodeName = $this->params->get('node')) {
+ return $bp->getNode($nodeName);
+ } else {
+ return null;
+ }
+ }
+
+ protected function prepareRenderer($bp, $node)
+ {
+ if ($this->renderer === null) {
+ if ($this->params->get('mode') === 'tree') {
+ $renderer = new TreeRenderer($bp, $node);
+ } else {
+ $renderer = new TileRenderer($bp, $node);
+ }
+ $renderer->setUrl($this->url())
+ ->setPath($this->params->getValues('path'));
+
+ $this->renderer = $renderer;
+ }
+
+ return $this->renderer;
+ }
+
+ protected function getProcessTabs(BpConfig $bp, Renderer $renderer)
+ {
+ $tabs = $this->singleTab($bp->getTitle());
+ if ($renderer->isLocked()) {
+ $tabs->extend(new DashboardAction());
+ }
+
+ return $tabs;
+ }
+
+ protected function handleSimulations(BpConfig $bp)
+ {
+ $simulation = Simulation::fromSession($this->session());
+
+ if ($this->params->get('dismissSimulations')) {
+ Notification::success(
+ sprintf(
+ $this->translate('%d applied simulation(s) have been dropped'),
+ $simulation->count()
+ )
+ );
+ $simulation->clear();
+ $this->redirectNow($this->url()->without('dismissSimulations')->without('unlocked'));
+ }
+
+ $bp->applySimulation($simulation);
+ }
+
+ protected function loadActionForm(BpConfig $bp, Node $node = null)
+ {
+ $action = $this->params->get('action');
+ $form = null;
+ if ($this->showFullscreen) {
+ return;
+ }
+
+ $canEdit = $bp->getMetadata()->canModify();
+
+ if ($action === 'add' && $canEdit) {
+ $form = (new AddNodeForm())
+ ->setProcess($bp)
+ ->setParentNode($node)
+ ->setStorage($this->storage())
+ ->setSession($this->session())
+ ->on(AddNodeForm::ON_SUCCESS, function () {
+ $this->redirectNow(Url::fromRequest()->without('action'));
+ })
+ ->handleRequest($this->getServerRequest());
+
+ if ($form->hasElement('children')) {
+ /** @var TermInput $childrenElement */
+ $childrenElement = $form->getElement('children');
+ foreach ($childrenElement->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'))
+ ->setProcess($bp)
+ ->setSession($this->session())
+ ->handleRequest();
+ } elseif ($action === 'editmonitored' && $canEdit) {
+ $form = (new EditNodeForm())
+ ->setProcess($bp)
+ ->setNode($bp->getNode($this->params->get('editmonitorednode')))
+ ->setParentNode($node)
+ ->setSession($this->session())
+ ->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'))
+ ->setProcess($bp)
+ ->setNode($bp->getNode($this->params->get('deletenode')))
+ ->setParentNode($node)
+ ->setSession($this->session())
+ ->handleRequest();
+ } elseif ($action === 'edit' && $canEdit) {
+ $form = $this->loadForm('Process')
+ ->setSuccessUrl(Url::fromRequest()->without('action'))
+ ->setProcess($bp)
+ ->setNode($bp->getNode($this->params->get('editnode')))
+ ->setSession($this->session())
+ ->handleRequest();
+ } elseif ($action === 'simulation') {
+ $form = $this->loadForm('simulation')
+ ->setSuccessUrl(Url::fromRequest()->without('action'))
+ ->setNode($bp->getNode($this->params->get('simulationnode')))
+ ->setSimulation(Simulation::fromSession($this->session()))
+ ->handleRequest();
+ } elseif ($action === 'move') {
+ $successUrl = $this->url()->without(['action', 'movenode']);
+ if ($this->params->get('mode') === 'tree') {
+ // If the user moves a node from a subtree, the `node` param exists
+ $successUrl->getParams()->remove('node');
+ }
+
+ if ($this->session()->get('sort.default')) {
+ // If there's a default sort specification in the session, it can only be `display_name desc`,
+ // as otherwise the user wouldn't be able to trigger this action. So it's safe to just define
+ // descending manual order now.
+ $successUrl->getParams()->add(SortControl::DEFAULT_SORT_PARAM, 'manual desc');
+ }
+
+ $form = $this->loadForm('MoveNode')
+ ->setSuccessUrl($successUrl)
+ ->setProcess($bp)
+ ->setParentNode($node)
+ ->setSession($this->session())
+ ->setNode($bp->getNode($this->params->get('movenode')))
+ ->handleRequest();
+ }
+
+ if ($form) {
+ $this->content()->prepend(HtmlString::create((string) $form));
+ }
+ }
+
+ protected function setDynamicAutorefresh()
+ {
+ if (! $this->isXhr()) {
+ // This will trigger the very first XHR refresh immediately on page
+ // load. Please not that this may hammer the server in case we would
+ // decide to use autorefreshInterval for HTML meta-refreshes also.
+ $this->setAutorefreshInterval(1);
+ return;
+ }
+
+ 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);
+ }
+ }
+
+ protected function showWarnings(BpConfig $bp)
+ {
+ if ($bp->hasWarnings()) {
+ $ul = Html::tag('ul', array('class' => 'warning'));
+ foreach ($bp->getWarnings() as $warning) {
+ $ul->add(Html::tag('li')->setContent($warning));
+ }
+
+ return $ul;
+ } else {
+ return null;
+ }
+ }
+
+ protected function showErrors(BpConfig $bp)
+ {
+ if ($bp->hasWarnings()) {
+ $ul = Html::tag('ul', array('class' => 'error'));
+ foreach ($bp->getErrors() as $msg) {
+ $ul->add(Html::tag('li')->setContent($msg));
+ }
+
+ return $ul;
+ } else {
+ return null;
+ }
+ }
+
+ protected function showHints(BpConfig $bp, Renderer $renderer)
+ {
+ $ul = Html::tag('ul', ['class' => 'error']);
+ $this->prepareMissingNodeLinks($ul);
+ foreach ($bp->getErrors() as $error) {
+ $ul->addHtml(Html::tag('li', $error));
+ }
+
+ if ($bp->hasChanges()) {
+ $li = Html::tag('li')->setSeparator(' ');
+ $li->add(sprintf(
+ $this->translate('This process has %d pending change(s).'),
+ $bp->countChanges()
+ ))->add(Html::tag(
+ 'a',
+ [
+ 'href' => Url::fromPath('businessprocess/process/config')
+ ->setParams($this->getRequest()->getUrl()->getParams())
+ ],
+ $this->translate('Store')
+ ))->add(Html::tag(
+ 'a',
+ ['href' => $this->url()->with('dismissChanges', true)],
+ $this->translate('Dismiss')
+ ));
+ $ul->add($li);
+ }
+
+ if ($bp->hasSimulations()) {
+ $li = Html::tag('li')->setSeparator(' ');
+ $li->add(sprintf(
+ $this->translate('This process shows %d simulated state(s).'),
+ $bp->countSimulations()
+ ))->add(Html::tag(
+ 'a',
+ ['href' => $this->url()->with('dismissSimulations', true)],
+ $this->translate('Dismiss')
+ ));
+ $ul->add($li);
+ }
+
+ if (! $renderer->isLocked() && $renderer->appliesCustomSorting()) {
+ $ul->addHtml(Html::tag('li', null, [
+ Text::create($this->translate('Drag&Drop disabled. Custom sort order applied.')),
+ (new Form())
+ ->setAttribute('class', 'inline')
+ ->addElement('submitButton', SortControl::DEFAULT_SORT_PARAM, [
+ 'label' => $this->translate('Reset to default'),
+ 'value' => $renderer->getDefaultSort(),
+ 'class' => 'link-button'
+ ])
+ ->addElement('hidden', 'uid', ['value' => 'bp-sort-control'])
+ ])->setSeparator(' '));
+ }
+
+ if (! $ul->isEmpty()) {
+ return $ul;
+ } else {
+ return null;
+ }
+ }
+
+ protected function prepareMissingNodeLinks(HtmlElement $ul): void
+ {
+ $missing = array_keys($this->bp->getMissingChildren());
+ if (! empty($missing)) {
+ $missingLinkedNodes = null;
+ foreach ($this->bp->getImportedNodes() as $process) {
+ if ($process->hasMissingChildren()) {
+ $missingLinkedNodes = array_keys($process->getMissingChildren());
+ $link = Url::fromPath('businessprocess/process/show')
+ ->addParams(['config' => $process->getConfigName()]);
+
+ $ul->addHtml(Html::tag(
+ 'li',
+ [
+ TemplateString::create(
+ tp(
+ 'Linked node %s has one missing child node: {{#link}}Show{{/link}}',
+ 'Linked node %s has %d missing child nodes: {{#link}}Show{{/link}}',
+ count($missingLinkedNodes)
+ ),
+ $process->getAlias(),
+ count($missingLinkedNodes),
+ ['link' => new Link(null, (string) $link)]
+ )
+ ]
+ ));
+ }
+ }
+
+ if (! empty($missingLinkedNodes)) {
+ return;
+ }
+
+ $count = count($missing);
+ if ($count > 10) {
+ $missing = array_slice($missing, 0, 10);
+ $missing[] = '...';
+ }
+
+ $link = Url::fromPath('businessprocess/process/show')
+ ->addParams(['config' => $this->bp->getName(), 'action' => 'cleanup']);
+
+ $ul->addHtml(Html::tag(
+ 'li',
+ [
+ TemplateString::create(
+ tp(
+ '{{#link}}Cleanup{{/link}} one missing node: %2$s',
+ '{{#link}}Cleanup{{/link}} %d missing nodes: %s',
+ count($missing)
+ ),
+ ['link' => new Link(null, (string) $link)],
+ $count,
+ implode(', ', $missing)
+ )
+ ]
+ ));
+ }
+ }
+
+ /**
+ * Show the source code for a process
+ */
+ public function sourceAction()
+ {
+ $this->assertPermission('businessprocess/modify');
+
+ $bp = $this->loadModifiedBpConfig();
+ $this->view->showDiff = $showDiff = (bool) $this->params->get('showDiff', false);
+
+ $this->view->source = LegacyConfigRenderer::renderConfig($bp);
+ if ($this->view->showDiff) {
+ $this->view->diff = ConfigDiff::create(
+ $this->storage()->getSource($this->view->configName),
+ $this->view->source
+ );
+ $title = sprintf(
+ $this->translate('%s: Source Code Differences'),
+ $bp->getTitle()
+ );
+ } else {
+ $title = sprintf(
+ $this->translate('%s: Source Code'),
+ $bp->getTitle()
+ );
+ }
+
+ $this->setTitle($title);
+ $this->controls()
+ ->add($this->tabsForConfig($bp)->activate('source'))
+ ->add(Html::tag('h1', null, $title))
+ ->add($this->createConfigActionBar($bp, $showDiff));
+
+ $this->setViewScript('process/source');
+ }
+
+ /**
+ * Download a process configuration file
+ */
+ public function downloadAction()
+ {
+ $this->assertPermission('businessprocess/modify');
+
+ $config = $this->loadModifiedBpConfig();
+ $response = $this->getResponse();
+ $response->setHeader(
+ 'Content-Disposition',
+ sprintf(
+ 'attachment; filename="%s.conf";',
+ $config->getName()
+ )
+ );
+ $response->setHeader('Content-Type', 'text/plain');
+
+ echo LegacyConfigRenderer::renderConfig($config);
+ $this->doNotRender();
+ }
+
+ /**
+ * Modify a business process configuration
+ */
+ public function configAction()
+ {
+ $this->assertPermission('businessprocess/modify');
+
+ $bp = $this->loadModifiedBpConfig();
+
+ $title = sprintf(
+ $this->translate('%s: Configuration'),
+ $bp->getTitle()
+ );
+ $this->setTitle($title);
+ $this->controls()
+ ->add($this->tabsForConfig($bp)->activate('config'))
+ ->add(Html::tag('h1', null, $title))
+ ->add($this->createConfigActionBar($bp));
+
+ $url = Url::fromPath('businessprocess/process/show')
+ ->setParams($this->getRequest()->getUrl()->getParams());
+ $this->content()->add(
+ $this->loadForm('bpConfig')
+ ->setProcess($bp)
+ ->setStorage($this->storage())
+ ->setSuccessUrl($url)
+ ->handleRequest()
+ );
+ }
+
+ protected function createConfigActionBar(BpConfig $config, $showDiff = false)
+ {
+ $actionBar = new ActionBar();
+
+ if ($showDiff) {
+ $params = array('config' => $config->getName());
+ $actionBar->add(Html::tag(
+ 'a',
+ [
+ 'href' => Url::fromPath('businessprocess/process/source', $params),
+ 'title' => $this->translate('Show source code')
+ ],
+ [
+ new Icon('file-lines'),
+ $this->translate('Source'),
+ ]
+ ));
+ } else {
+ $params = array(
+ 'config' => $config->getName(),
+ 'showDiff' => true
+ );
+
+ $actionBar->add(Html::tag(
+ 'a',
+ [
+ 'href' => Url::fromPath('businessprocess/process/source', $params),
+ 'title' => $this->translate('Highlight changes')
+ ],
+ [
+ new Icon('shuffle'),
+ $this->translate('Diff')
+ ]
+ ));
+ }
+
+ $actionBar->add(Html::tag(
+ 'a',
+ [
+ 'href' => Url::fromPath('businessprocess/process/download', ['config' => $config->getName()]),
+ 'target' => '_blank',
+ 'title' => $this->translate('Download process configuration')
+ ],
+ [
+ new Icon('download'),
+ $this->translate('Download')
+ ]
+ ));
+
+ return $actionBar;
+ }
+
+ protected function tabsForShow()
+ {
+ return $this->tabs()->add('show', array(
+ 'label' => $this->translate('Business Process'),
+ 'url' => $this->url()
+ ));
+ }
+
+ /**
+ * @return Tabs
+ */
+ protected function tabsForCreate()
+ {
+ return $this->tabs()->add('create', array(
+ 'label' => $this->translate('Create'),
+ 'url' => 'businessprocess/process/create'
+ ))->add('upload', array(
+ 'label' => $this->translate('Upload'),
+ 'url' => 'businessprocess/process/upload'
+ ));
+ }
+
+ protected function tabsForConfig(BpConfig $config)
+ {
+ $params = array(
+ 'config' => $config->getName()
+ );
+
+ $tabs = $this->tabs()->add('config', array(
+ 'label' => $this->translate('Process Configuration'),
+ 'url' =>Url::fromPath('businessprocess/process/config', $params)
+ ));
+
+ if ($this->params->get('showDiff')) {
+ $params['showDiff'] = true;
+ }
+
+ $tabs->add('source', array(
+ 'label' => $this->translate('Source'),
+ 'url' =>Url::fromPath('businessprocess/process/source', $params)
+ ));
+
+ return $tabs;
+ }
+
+ protected function handleFormatRequest(BpConfig $bp, BpNode $node = null)
+ {
+ $desiredContentType = $this->getRequest()->getHeader('Accept');
+ if ($desiredContentType === 'application/json') {
+ $desiredFormat = 'json';
+ } elseif ($desiredContentType === 'text/csv') {
+ $desiredFormat = 'csv';
+ } else {
+ $desiredFormat = strtolower($this->params->get('format', 'html'));
+ }
+
+ switch ($desiredFormat) {
+ case 'json':
+ $response = $this->getResponse();
+ $response
+ ->setHeader('Content-Type', 'application/json')
+ ->setHeader('Cache-Control', 'no-store')
+ ->setHeader(
+ 'Content-Disposition',
+ 'inline; filename=' . $this->getRequest()->getActionName() . '.json'
+ )
+ ->appendBody(Json::sanitize($node !== null ? $node->toArray() : $bp->toArray()))
+ ->sendResponse();
+ exit;
+ case 'csv':
+ $csv = fopen('php://temp', 'w');
+
+ fputcsv($csv, ['Path', 'Name', 'State', 'Since', 'In_Downtime']);
+
+ foreach ($node !== null ? $node->toArray(null, true) : $bp->toArray(true) as $node) {
+ $data = [$node['path'], $node['name']];
+
+ if (isset($node['state'])) {
+ $data[] = $node['state'];
+ }
+
+ if (isset($node['since'])) {
+ $data[] = DateFormatter::formatDateTime($node['since']);
+ }
+
+ if (isset($node['in_downtime'])) {
+ $data[] = $node['in_downtime'];
+ }
+
+ fputcsv($csv, $data);
+ }
+
+ $response = $this->getResponse();
+ $response
+ ->setHeader('Content-Type', 'text/csv')
+ ->setHeader('Cache-Control', 'no-store')
+ ->setHeader(
+ 'Content-Disposition',
+ 'attachment; filename=' . $this->getRequest()->getActionName() . '.csv'
+ )
+ ->sendHeaders();
+
+ rewind($csv);
+
+ fpassthru($csv);
+
+ exit;
+ }
+ }
+}
diff --git a/application/controllers/ServiceController.php b/application/controllers/ServiceController.php
new file mode 100644
index 0000000..671d00c
--- /dev/null
+++ b/application/controllers/ServiceController.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Controllers;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Businessprocess\IcingaDbObject;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\DataView\DataView;
+use Icinga\Web\Url;
+use ipl\Stdlib\Filter;
+
+class ServiceController extends Controller
+{
+ /**
+ * True if business process prefers to use icingadb as backend for it's nodes
+ *
+ * @var bool
+ */
+ protected $isIcingadbPreferred;
+
+ protected function moduleInit()
+ {
+ $this->isIcingadbPreferred = Module::exists('icingadb')
+ && ! $this->params->has('backend')
+ && IcingadbSupport::useIcingaDbAsBackend();
+
+ if (! $this->isIcingadbPreferred) {
+ parent::moduleInit();
+ }
+ }
+
+ public function showAction()
+ {
+ if ($this->isIcingadbPreferred) {
+ $hostName = $this->params->shift('host');
+ $serviceName = $this->params->shift('service');
+
+ $query = Service::on(IcingaDbObject::fetchDb());
+ IcingaDbObject::applyIcingaDbRestrictions($query);
+
+ $query->filter(Filter::all(
+ Filter::equal('service.name', $serviceName),
+ Filter::equal('host.name', $hostName)
+ ));
+
+ $service = $query->first();
+
+ $this->params->add('name', $serviceName);
+ $this->params->add('host.name', $hostName);
+
+ if ($service !== null) {
+ $this->redirectNow(Url::fromPath('icingadb/service')->setParams($this->params));
+ }
+ } else {
+ $hostName = $this->params->get('host');
+ $serviceName = $this->params->get('service');
+
+ $query = $this->backend->select()
+ ->from('servicestatus', array('service_description'))
+ ->where('host_name', $hostName)
+ ->where('service_description', $serviceName);
+
+ $this->applyRestriction('monitoring/filter/objects', $query);
+ if ($query->fetchRow() !== false) {
+ $this->redirectNow(Url::fromPath('monitoring/service/show')->setParams($this->params));
+ }
+ }
+
+ $this->view->host = $hostName;
+ $this->view->service = $serviceName;
+ }
+}
diff --git a/application/controllers/SuggestionsController.php b/application/controllers/SuggestionsController.php
new file mode 100644
index 0000000..9fa0331
--- /dev/null
+++ b/application/controllers/SuggestionsController.php
@@ -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()));
+ }
+}