diff --git a/application/controllers/MigrateController.php b/application/controllers/MigrateController.php index 899f1f8f..014c3328 100644 --- a/application/controllers/MigrateController.php +++ b/application/controllers/MigrateController.php @@ -7,12 +7,15 @@ namespace Icinga\Module\Icingadb\Controllers; 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 @@ class MigrateController extends Controller $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 @@ class MigrateController extends Controller } $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 416d37f7..2c83acc1 100644 --- a/configuration.php +++ b/configuration.php @@ -565,9 +565,5 @@ namespace Icinga\Module\Icingadb { $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/public/css/widget/migrate-popup.less b/public/css/widget/migrate-popup.less index f7adb22f..8f9586b9 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 9984d3a3..5a00dbee 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();