diff --git a/application/controllers/MigrateController.php b/application/controllers/MigrateController.php index 899f1f8fe..014c3328e 100644 --- a/application/controllers/MigrateController.php +++ b/application/controllers/MigrateController.php @@ -7,12 +7,15 @@ use Exception; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Application\Hook; +use Icinga\Application\Icinga; use Icinga\Exception\IcingaException; use Icinga\Module\Icingadb\Compat\UrlMigrator; use Icinga\Module\Icingadb\Forms\SetAsBackendForm; use Icinga\Module\Icingadb\Hook\IcingadbSupportHook; use Icinga\Module\Icingadb\Web\Controller; use ipl\Html\HtmlString; +use ipl\Stdlib\Filter; +use ipl\Web\Filter\QueryString; use ipl\Web\Url; class MigrateController extends Controller @@ -65,6 +68,55 @@ public function monitoringUrlAction() $response->sendResponse(); } + public function searchUrlAction() + { + $this->assertHttpMethod('post'); + if (! $this->getRequest()->isApiRequest()) { + $this->httpBadRequest('No API request'); + } + + if ( + ! preg_match('/([^;]*);?/', $this->getRequest()->getHeader('Content-Type'), $matches) + || $matches[1] !== 'application/json' + ) { + $this->httpBadRequest('No JSON content'); + } + + $traverseFilter = function ($filter) use (&$traverseFilter) { + if ($filter instanceof Filter\Chain) { + foreach ($filter as $child) { + $newChild = $traverseFilter($child); + if ($newChild !== null) { + $filter->replace($child, $newChild); + } + } + } elseif ($filter instanceof Filter\Equal) { + if (strpos($filter->getValue(), '*') !== false) { + return Filter::like($filter->getColumn(), $filter->getValue()); + } + } elseif ($filter instanceof Filter\Unequal) { + if (strpos($filter->getValue(), '*') !== false) { + return Filter::unlike($filter->getColumn(), $filter->getValue()); + } + } + }; + + $urls = $this->getRequest()->getPost(); + + $result = []; + foreach ($urls as $urlString) { + $url = Url::fromPath($urlString); + $filter = QueryString::parse($url->getQueryString()); + $filter = $traverseFilter($filter) ?? $filter; + $result[] = $url->setQueryString(QueryString::render($filter))->getAbsoluteUrl(); + } + + $response = $this->getResponse()->json(); + $response->setSuccessData($result); + + $response->sendResponse(); + } + public function checkboxStateAction() { $this->assertHttpMethod('get'); @@ -98,7 +150,10 @@ public function backendSupportAction() } $moduleSupportStates = []; - if ($this->Auth()->hasPermission('module/monitoring')) { + if ( + Icinga::app()->getModuleManager()->hasEnabled('monitoring') + && $this->Auth()->hasPermission('module/monitoring') + ) { $supportList = []; foreach (Hook::all('Icingadb/IcingadbSupport') as $hook) { /** @var IcingadbSupportHook $hook */ diff --git a/configuration.php b/configuration.php index 416d37f78..2c83acc1c 100644 --- a/configuration.php +++ b/configuration.php @@ -565,9 +565,5 @@ $this->provideJsFile('action-list.js'); $this->provideJsFile('loadmore.js'); - - $mg = Icinga::app()->getModuleManager(); - if ($mg->hasEnabled('monitoring')) { - $this->provideJsFile('migrate.js'); - } + $this->provideJsFile('migrate.js'); } diff --git a/library/Icingadb/Common/StateBadges.php b/library/Icingadb/Common/StateBadges.php index 2cb5cc8da..a4e58dbe5 100644 --- a/library/Icingadb/Common/StateBadges.php +++ b/library/Icingadb/Common/StateBadges.php @@ -4,7 +4,6 @@ namespace Icinga\Module\Icingadb\Common; -use Icinga\Data\Filter\Filter; use Icinga\Module\Icingadb\Widget\StateBadge; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; @@ -116,7 +115,9 @@ public function createLink($content, array $params = null): Link } if ($this->hasBaseFilter()) { - $url->addFilter(Filter::fromQueryString(QueryString::render($this->getBaseFilter()))); + $urlParams = $url->getParams()->toArray(false); + $url->setQueryString(QueryString::render($this->getBaseFilter())) + ->addParams($urlParams); } return new Link($content, $url); diff --git a/library/Icingadb/Widget/Detail/HostStatistics.php b/library/Icingadb/Widget/Detail/HostStatistics.php index 110eed2d5..18b0f400c 100644 --- a/library/Icingadb/Widget/Detail/HostStatistics.php +++ b/library/Icingadb/Widget/Detail/HostStatistics.php @@ -5,7 +5,6 @@ namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Chart\Donut; -use Icinga\Data\Filter\Filter; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Widget\HostStateBadges; use ipl\Html\ValidHtml; @@ -38,7 +37,7 @@ protected function createTotal(): ValidHtml { $url = Links::hosts(); if ($this->hasBaseFilter()) { - $url->addFilter(Filter::fromQueryString(QueryString::render($this->getBaseFilter()))); + $url->setQueryString(QueryString::render($this->getBaseFilter())); } return new Link( diff --git a/library/Icingadb/Widget/Detail/ServiceStatistics.php b/library/Icingadb/Widget/Detail/ServiceStatistics.php index 6b6ed9d22..9f0cbdf67 100644 --- a/library/Icingadb/Widget/Detail/ServiceStatistics.php +++ b/library/Icingadb/Widget/Detail/ServiceStatistics.php @@ -5,7 +5,6 @@ namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Chart\Donut; -use Icinga\Data\Filter\Filter; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Widget\ServiceStateBadges; use ipl\Html\ValidHtml; @@ -42,7 +41,7 @@ protected function createTotal(): ValidHtml { $url = Links::services(); if ($this->hasBaseFilter()) { - $url->addFilter(Filter::fromQueryString(QueryString::render($this->getBaseFilter()))); + $url->setQueryString(QueryString::render($this->getBaseFilter())); } return new Link( diff --git a/library/Icingadb/Widget/HostSummaryDonut.php b/library/Icingadb/Widget/HostSummaryDonut.php index 19f7984f3..79521129f 100644 --- a/library/Icingadb/Widget/HostSummaryDonut.php +++ b/library/Icingadb/Widget/HostSummaryDonut.php @@ -5,7 +5,6 @@ namespace Icinga\Module\Icingadb\Widget; use Icinga\Chart\Donut; -use Icinga\Data\Filter\Filter; use Icinga\Module\Icingadb\Common\BaseFilter; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\HoststateSummary; @@ -42,9 +41,7 @@ protected function assembleBody(BaseHtmlElement $body) ->addSlice($this->summary->hosts_unreachable_unhandled, ['class' => 'slice-state-unreachable']) ->addSlice($this->summary->hosts_pending, ['class' => 'slice-state-pending']) ->setLabelBig($this->summary->hosts_down_unhandled) - ->setLabelBigUrl(Links::hosts()->addFilter( - Filter::fromQueryString(QueryString::render($this->getBaseFilter())) - )->addParams([ + ->setLabelBigUrl(Links::hosts()->setQueryString(QueryString::render($this->getBaseFilter()))->addParams([ 'host.state.soft_state' => 1, 'host.state.is_handled' => 'n', 'sort' => 'host.state.last_state_change' diff --git a/library/Icingadb/Widget/ServiceSummaryDonut.php b/library/Icingadb/Widget/ServiceSummaryDonut.php index 8141f86b8..4cb861feb 100644 --- a/library/Icingadb/Widget/ServiceSummaryDonut.php +++ b/library/Icingadb/Widget/ServiceSummaryDonut.php @@ -5,7 +5,6 @@ namespace Icinga\Module\Icingadb\Widget; use Icinga\Chart\Donut; -use Icinga\Data\Filter\Filter; use Icinga\Module\Icingadb\Common\BaseFilter; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\ServicestateSummary; @@ -44,9 +43,7 @@ protected function assembleBody(BaseHtmlElement $body) ->addSlice($this->summary->services_unknown_unhandled, ['class' => 'slice-state-unknown']) ->addSlice($this->summary->services_pending, ['class' => 'slice-state-pending']) ->setLabelBig($this->summary->services_critical_unhandled) - ->setLabelBigUrl(Links::services()->addFilter( - Filter::fromQueryString(QueryString::render($this->getBaseFilter())) - )->addParams([ + ->setLabelBigUrl(Links::services()->setQueryString(QueryString::render($this->getBaseFilter()))->addParams([ 'service.state.soft_state' => 2, 'service.state.is_handled' => 'n', 'sort' => 'service.state.last_state_change' diff --git a/public/css/widget/migrate-popup.less b/public/css/widget/migrate-popup.less index f7adb22f6..8f9586b95 100644 --- a/public/css/widget/migrate-popup.less +++ b/public/css/widget/migrate-popup.less @@ -76,6 +76,9 @@ } .suggestion-area { + display: flex; + flex-direction: column-reverse; + padding: .75em; flex-grow: 1; pointer-events: auto; @@ -90,12 +93,19 @@ } p { + display: none; margin-bottom: .5em; color: @text-color-light; } + form ~ .monitoring-migration-hint, + .search-migration-suggestions:not(:empty) + .search-migration-hint, + .monitoring-migration-suggestions:not(:empty) + .monitoring-migration-hint { + display: block; + } + & > button.close { - float: right; + margin-left: auto; margin-top: 1em; &:hover { @@ -105,7 +115,7 @@ ul { padding: 0; - margin: .5em 0 0; + margin: 0; list-style-type: none; } @@ -116,6 +126,10 @@ &:last-of-type { margin-bottom: 0; } + + &:first-of-type { + margin-top: 0; + } } li { @@ -143,7 +157,6 @@ } form { - margin-top: 0.5em; width: 100%; .control-group { @@ -159,5 +172,10 @@ } } } + + .search-migration-suggestions:not(:empty) ~ form, + .search-migration-suggestions:not(:empty) ~ .monitoring-migration-suggestions:not(:empty) { + margin-bottom: .5em; + } } } diff --git a/public/js/migrate.js b/public/js/migrate.js index 9984d3a3b..5a00dbee1 100644 --- a/public/js/migrate.js +++ b/public/js/migrate.js @@ -11,9 +11,11 @@ const POPUP_HTML = '
\n' + '
\n' + '
\n' + - '

Preview this in Icinga DB

\n' + - '
    \n' + ' \n' + + '
      \n' + + '

      Miss some results? Try the link(s) below

      \n' + + '
        \n' + + '

        Preview this in Icinga DB

        \n' + '
        \n' + '
        \n' + '
        \n' + @@ -37,6 +39,7 @@ this.knownBackendSupport = {}; this.urlMigrationReadyState = null; this.backendSupportReadyState = null; + this.searchMigrationReadyState = null; this.backendSupportRelated = {}; this.$popup = null; @@ -48,6 +51,7 @@ // We don't want to ask the server to migrate non-monitoring urls this.isMonitoringUrl = new RegExp('^' + icinga.config.baseUrl + '/monitoring/'); + this.on('rendered', this.onRendered, this); this.on('close-column', this.onColumnClose, this); this.on('click', '#migrate-popup button.close', this.onClose, this); @@ -107,7 +111,8 @@ }; Migrate.prototype.prepareMigration = function($target) { - let urls = {}; + let monitoringUrls = {}; + let searchUrls = {}; let modules = {} $target.each((_, container) => { @@ -115,14 +120,18 @@ let href = $container.data('icingaUrl'); let containerId = $container.attr('id'); - if (typeof href !== 'undefined' && href.match(this.isMonitoringUrl)) { + if (!! href) { if ( typeof this.previousMigrations[containerId] !== 'undefined' && this.previousMigrations[containerId] === href ) { delete this.previousMigrations[containerId]; } else { - urls[containerId] = href; + if (href.match(this.isMonitoringUrl)) { + monitoringUrls[containerId] = href; + } else if ($container.find('[data-enrichment-type="search-bar"]').length) { + searchUrls[containerId] = href; + } } } @@ -132,13 +141,20 @@ } }); - if (Object.keys(urls).length) { + if (Object.keys(monitoringUrls).length) { this.setUrlMigrationReadyState(false); - this.migrateMonitoringUrls(urls); + this.migrateUrls(monitoringUrls, 'monitoring'); } else { this.setUrlMigrationReadyState(null); } + if (Object.keys(searchUrls).length) { + this.setSearchMigrationReadyState(false); + this.migrateUrls(searchUrls, 'search'); + } else { + this.setSearchMigrationReadyState(null); + } + if (Object.keys(modules).length) { this.setBackendSupportReadyState(false); this.prepareBackendCheckboxForm(modules); @@ -146,7 +162,11 @@ this.setBackendSupportReadyState(null); } - if (this.urlMigrationReadyState === null && this.backendSupportReadyState === null) { + if ( + this.urlMigrationReadyState === null + && this.backendSupportReadyState === null + && this.searchMigrationReadyState === null + ) { this.cleanupPopup(); } }; @@ -221,7 +241,7 @@ _this.knownMigrations[containerUrl] = false; } - if (_this.Popup().find('li').length === 1) { + if (_this.Popup().find('li').length === 1 && ! _this.Popup().find('#setAsBackendForm').length) { _this.hidePopup(function () { // Let the transition finish first, looks cleaner $suggestion.remove(); @@ -246,7 +266,7 @@ } }; - Migrate.prototype.migrateMonitoringUrls = function(urls) { + Migrate.prototype.migrateUrls = function(urls, type) { var _this = this, containerIds = [], containerUrls = []; @@ -258,24 +278,34 @@ } }); + let endpoint, changeCallback; + if (type === 'monitoring') { + endpoint = 'monitoring-url'; + changeCallback = this.changeUrlMigrationReadyState.bind(this); + } else { + endpoint = 'search-url'; + changeCallback = this.changeSearchMigrationReadyState.bind(this); + } + if (containerUrls.length) { var req = $.ajax({ context : this, type : 'post', - url : this.icinga.config.baseUrl + '/icingadb/migrate/monitoring-url', + url : this.icinga.config.baseUrl + '/icingadb/migrate/' + endpoint, headers : { 'Accept': 'application/json' }, contentType : 'application/json', data : JSON.stringify(containerUrls) }); req.urls = urls; + req.suggestionType = type; req.urlIndexToContainerId = containerIds; req.done(this.processUrlMigrationResults); - req.always(() => this.changeUrlMigrationReadyState(true)); + req.always(() => changeCallback(true)); } else { // All urls have already been migrated once, show popup immediately - this.addSuggestions(urls); - this.changeUrlMigrationReadyState(true); + this.addSuggestions(urls, type); + changeCallback(true); } }; @@ -298,7 +328,7 @@ _this.knownMigrations[req.urls[containerId]] = migratedUrl; }); - this.addSuggestions(req.urls); + this.addSuggestions(req.urls, req.suggestionType); }; Migrate.prototype.prepareBackendCheckboxForm = function(modules) { @@ -373,7 +403,7 @@ $form.attr('data-base-target', 'migrate-popup-backend-submit-blackhole'); $form.append('
        '); - this.Popup().find('.suggestion-area > ul').after($form); + this.Popup().find('.monitoring-migration-suggestions').before($form); } else { let $newForm = $(html); $form.find('[name=backend]').prop('checked', $newForm.find('[name=backend]').is(':checked')); @@ -382,10 +412,17 @@ this.showPopup(); } - Migrate.prototype.addSuggestions = function(urls) { + Migrate.prototype.addSuggestions = function(urls, type) { + var where; + if (type === 'monitoring') { + where = '.monitoring-migration-suggestions'; + } else { + where = '.search-migration-suggestions'; + } + var _this = this, hasSuggestions = false, - $ul = this.Popup().find('.suggestion-area > ul'); + $ul = this.Popup().find('.suggestion-area > ul' + where); $.each(urls, function (containerId, containerUrl) { // No urls for which the user clicked "No" or an error occurred and only migrated urls please if (_this.knownMigrations[containerUrl] !== false && _this.knownMigrations[containerUrl] !== containerUrl) { @@ -425,6 +462,9 @@ if (hasSuggestions) { this.showPopup(); + if (type === 'search') { + this.maximizePopup(); + } } }; @@ -442,6 +482,8 @@ || _this.knownMigrations[containerUrl] === false // Already migrated or no migration necessary || containerUrl === _this.knownMigrations[containerUrl] + // The container URL changed + || containerUrl !== $suggestion.data('containerUrl') ) { toBeRemoved.push($suggestion); } @@ -478,7 +520,7 @@ let hasBackendForm = this.cleanupBackendForm(); if (hasBackendForm !== true && this.Popup().find('li').length === toBeRemoved.length) { - this.hidePopup(function () { + this.hidePopup(() => { // Let the transition finish first, looks cleaner $.each(toBeRemoved, function (_, $suggestion) { $suggestion.remove(); @@ -496,13 +538,20 @@ if (typeof hasBackendForm === 'object') { hasBackendForm.remove(); } + + // Let showPopup() handle the automatic minimization in case all search suggestions have been removed + this.showPopup(); } }; Migrate.prototype.showPopup = function() { var $popup = this.Popup(); - if (this.storage.get('minimized')) { - $popup.addClass('active minimized hidden'); + if (this.storage.get('minimized') && ! this.forceFullyMaximized()) { + if (this.isShown()) { + this.minimizePopup(); + } else { + $popup.addClass('active minimized hidden'); + } } else { $popup.addClass('active'); } @@ -532,6 +581,10 @@ this.Popup().removeClass('minimized hidden'); }; + Migrate.prototype.forceFullyMaximized = function() { + return this.Popup().find('.search-migration-suggestions:not(:empty)').length > 0; + }; + Migrate.prototype.togglePopup = function() { if (this.Popup().is('.minimized')) { this.maximizePopup(); @@ -549,7 +602,23 @@ Migrate.prototype.changeUrlMigrationReadyState = function (state) { this.setUrlMigrationReadyState(state); - if (this.backendSupportReadyState !== false) { + if (this.backendSupportReadyState !== false && this.searchMigrationReadyState !== false) { + this.searchMigrationReadyState = null; + this.backendSupportReadyState = null; + this.urlMigrationReadyState = null; + this.cleanupPopup(); + } + }; + + Migrate.prototype.setSearchMigrationReadyState = function (state) { + this.searchMigrationReadyState = state; + }; + + Migrate.prototype.changeSearchMigrationReadyState = function (state) { + this.setSearchMigrationReadyState(state); + + if (this.backendSupportReadyState !== false && this.urlMigrationReadyState !== false) { + this.searchMigrationReadyState = null; this.backendSupportReadyState = null; this.urlMigrationReadyState = null; this.cleanupPopup(); @@ -563,7 +632,8 @@ Migrate.prototype.changeBackendSupportReadyState = function (state) { this.setBackendSupportReadyState(state); - if (this.urlMigrationReadyState !== false) { + if (this.urlMigrationReadyState !== false && this.searchMigrationReadyState !== false) { + this.searchMigrationReadyState = null; this.backendSupportReadyState = null; this.urlMigrationReadyState = null; this.cleanupPopup();