migrate.js: Enhance detection of modules with support for Icinga DB

This commit is contained in:
Johannes Meyer 2021-11-11 16:17:58 +01:00
parent 59e3d79c36
commit a098d7e6fc
4 changed files with 263 additions and 89 deletions

View file

@ -8,7 +8,6 @@ use Icinga\Application\Config;
use Icinga\Module\Icingadb\Forms\SetAsBackendConfigForm;
use Icinga\Module\Icingadb\Forms\DatabaseConfigForm;
use Icinga\Module\Icingadb\Forms\RedisConfigForm;
use Icinga\Module\Icingadb\Forms\SetAsBackendForm;
use Icinga\Module\Icingadb\Web\Controller;
use Icinga\Web\Form;
use Icinga\Web\Widget\Tab;
@ -50,11 +49,6 @@ class ConfigController extends Controller
public function backendAction()
{
// migration.js
(new SetAsBackendForm())
->setRedirectUrl('__BACK__')
->handleRequest();
$form = new SetAsBackendConfigForm();
$form->handleRequest();

View file

@ -7,8 +7,11 @@ namespace Icinga\Module\Icingadb\Controllers;
use Exception;
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 Icinga\Web\Hook;
use ipl\Html\HtmlString;
use ipl\Web\Url;
class MigrateController extends Controller
@ -70,4 +73,52 @@ class MigrateController extends Controller
->sendResponse();
exit;
}
public function checkboxSubmitAction()
{
$this->assertHttpMethod('post');
$form = (new SetAsBackendForm())
->setOnSuccess(function () use (&$form) {
$this->addPart(HtmlString::create('"bogus"'), 'Behavior:Migrate');
$form->save($form->getElement('backend')->isChecked());
return false;
});
$form->handleRequest();
}
public function backendSupportAction()
{
$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');
}
$supportList = [];
foreach (Hook::all('Icingadb/IcingadbSupport') as $hook) {
/** @var IcingadbSupportHook $hook */
$supportList[$hook->getModule()->getName()] = $hook->supportsIcingaDb();
}
$moduleSupportStates = [];
foreach ($this->getRequest()->getPost() as $moduleName) {
if (isset($supportList[$moduleName])) {
$moduleSupportStates[] = $supportList[$moduleName];
} else {
$moduleSupportStates[] = false;
}
}
$this->getResponse()
->json()
->setSuccessData($moduleSupportStates)
->sendResponse();
}
}

View file

@ -20,9 +20,4 @@ class SetAsBackendForm extends SetAsBackendConfigForm
$this->removeElement('btn_submit');
$this->removeElement('btn_submit_session');
}
public function onSuccess()
{
$this->save($this->getElement('backend')->isChecked());
}
}

View file

@ -24,7 +24,9 @@
' <button type="button" value="0"><i class="icon-"></i></button>\n' +
'</li>';
const BACKEND_FORM = '<form id="setAsBackendForm" name="IcingaModuleIcingadbFormsSetAsBackendForm" class="icinga-form icinga-controls" method="post" action="icingadb/config/backend">\n' +
const BACKEND_FORM = '<form id="setAsBackendForm" name="IcingaModuleIcingadbFormsSetAsBackendForm" ' +
'class="icinga-form icinga-controls" method="post" action="icingadb/migrate/checkbox-submit" ' +
'data-base-target="migrate-popup-backend-submit-blackhole">\n' +
' <div class="wrapper">\n' +
' <div class="checkbox-label">\n' +
' <span><label for="setAsBackendForm-checkbox">Use Icinga DB As Backend</label></span>\n' +
@ -35,6 +37,7 @@
' </label>\n' +
' </div>\n' +
' <input type="hidden" name="formUID" value="IcingaModuleIcingadbFormsSetAsBackendForm" id="IcingaModuleIcingadbFormsSetAsBackendForm">\n' +
' <div id="migrate-popup-backend-submit-blackhole"></div>\n' +
'</form>';
/**
@ -47,13 +50,11 @@
this.icinga = icinga;
this.knownMigrations = {};
this.knownBackendSupport = {};
this.urlMigrationReadyState = null;
this.backendSupportReadyState = null;
this.backendSupportRelated = {};
this.$popup = null;
this.supportedModules = [
'/director',
'/businessprocess',
'/jira/',
'/cube/'
];
// Some persistence, we don't want to annoy our users too much
this.storage = Icinga.Storage.BehaviorStorage('icingadb.migrate');
@ -63,7 +64,6 @@
// We don't want to ask the server to migrate non-monitoring urls
this.isMonitoringUrl = new RegExp('^' + icinga.config.baseUrl + '/monitoring/');
this.isSupportedModule = new RegExp('^' + icinga.config.baseUrl + '(' + this.supportedModules.join('|') +')');
this.on('rendered', this.onRendered, this);
this.on('close-column', this.onColumnClose, this);
this.on('click', '#migrate-popup button.close', this.onClose, this);
@ -74,17 +74,32 @@
Migrate.prototype = new Icinga.EventListener();
Migrate.prototype.update = function (data) {
if (data !== 'bogus') {
return;
}
$.each(this.backendSupportRelated, (id, _) => {
let $container = $('#' + id);
let req = this.icinga.loader.loadUrl($container.data('icingaUrl'), $container);
req.addToHistory = false;
req.scripted = true;
});
};
Migrate.prototype.onRendered = function(event) {
var _this = event.data.self;
var $target = $(event.target);
if (! $target.is('#main > .container')) {
var attrUrl = $target.attr('data-icinga-url');
var dataUrl = $target.data('icingaUrl');
if (!! attrUrl && attrUrl !== dataUrl) {
// Search urls are redirected, update any migration suggestions
_this.prepareMigration($target);
return;
if ($target.is('#main .container')) {
var attrUrl = $target.attr('data-icinga-url');
var dataUrl = $target.data('icingaUrl');
if (!! attrUrl && attrUrl !== dataUrl) {
// Search urls are redirected, update any migration suggestions
_this.prepareMigration($target);
return;
}
}
// We are else really only interested in top-level containers
@ -108,41 +123,47 @@
};
Migrate.prototype.prepareMigration = function($target) {
var urls = {};
var href;
var wantBackendForm = false;
let urls = {};
let modules = {}
var _this = this;
$target.each(function () {
var $container = $(this);
href = $container.data('icingaUrl');
$target.each((_, container) => {
let $container = $(container);
let href = $container.data('icingaUrl');
let containerId = $container.attr('id');
if (typeof href !== 'undefined') {
if (href.match(_this.isMonitoringUrl)) {
var containerId = $container.attr('id');
if (
typeof _this.previousMigrations[containerId] !== 'undefined'
&& _this.previousMigrations[containerId] === href
) {
delete _this.previousMigrations[containerId];
} else {
urls[containerId] = href;
}
} else if (href.match(_this.isSupportedModule)) {
wantBackendForm = true;
if (typeof href !== 'undefined' && href.match(this.isMonitoringUrl)) {
if (
typeof this.previousMigrations[containerId] !== 'undefined'
&& this.previousMigrations[containerId] === href
) {
delete this.previousMigrations[containerId];
} else {
_this.removeBackendCheckboxForm();
urls[containerId] = href;
}
}
let moduleName = $container.data('icingaModule');
if (!! moduleName && moduleName !== 'default' && moduleName !== 'monitoring' && moduleName !== 'icingadb') {
modules[containerId] = moduleName;
}
});
if (this.icinga.utils.objectKeys(urls).length) {
if (Object.keys(urls).length) {
this.setUrlMigrationReadyState(false);
this.migrateMonitoringUrls(urls);
} else if (wantBackendForm) {
this.prepareBackendCheckboxForm();
} else {
this.cleanupSuggestions();
this.setUrlMigrationReadyState(null);
}
if (Object.keys(modules).length) {
this.setBackendSupportReadyState(false);
this.prepareBackendCheckboxForm(modules);
} else {
this.setBackendSupportReadyState(null);
}
if (this.urlMigrationReadyState === null && this.backendSupportReadyState === null) {
this.cleanupPopup();
}
};
@ -166,12 +187,26 @@
// Container moved
$suggestion.attr('id', 'suggest-' + $newContainer.attr('id'));
$suggestion.data('containerId', $newContainer.attr('id'));
} else {
// Container closed
$suggestion.remove();
}
}
});
let backendSupportRelated = { ..._this.backendSupportRelated };
$.each(backendSupportRelated, (id, module) => {
let $container = $('#' + id);
if (! $container.length || $container.data('icingaModule') !== module) {
let $newContainer = $('#main > .container').filter(function () {
return $(this).data('icingaModule') === module;
});
if ($newContainer.length) {
_this.backendSupportRelated[$newContainer.attr('id')] = module;
}
delete _this.backendSupportRelated[id];
}
});
_this.cleanupPopup();
};
Migrate.prototype.onClose = function(event) {
@ -232,7 +267,6 @@
containerIds = [],
containerUrls = [];
this.removeBackendCheckboxForm();
$.each(urls, function (containerId, containerUrl) {
if (typeof _this.knownMigrations[containerUrl] === 'undefined') {
containerUrls.push(containerUrl);
@ -253,11 +287,11 @@
req.urls = urls;
req.urlIndexToContainerId = containerIds;
req.done(this.processUrlMigrationResults);
req.always(_this.cleanupSuggestions);
req.always(() => this.changeUrlMigrationReadyState(true));
} else {
// All urls have already been migrated once, show popup immediately
this.addSuggestions(urls);
this.cleanupSuggestions();
this.changeUrlMigrationReadyState(true);
}
};
@ -283,43 +317,85 @@
this.addSuggestions(req.urls);
};
Migrate.prototype.prepareBackendCheckboxForm = function() {
var $popup = this.Popup();
var checkboxForm = $popup.find('.suggestion-area > #setAsBackendForm');
Migrate.prototype.prepareBackendCheckboxForm = function(modules) {
let containerIds = [];
let moduleNames = [];
this.cleanupSuggestions(); // remove links
if (! checkboxForm.length) {
$popup.find('.suggestion-area > ul').after($(BACKEND_FORM));
}
var req = $.ajax({
context : this,
type : 'get',
url : this.icinga.config.baseUrl + '/icingadb/migrate/checkbox-state'
$.each(modules, (id, module) => {
if (typeof this.knownBackendSupport[module] === 'undefined') {
containerIds.push(id);
moduleNames.push(module);
}
});
req.done(this.setCheckboxState);
if (moduleNames.length) {
let req = $.ajax({
context : this,
type : 'post',
url : this.icinga.config.baseUrl + '/icingadb/migrate/backend-support',
headers : { 'Accept': 'application/json' },
contentType : 'application/json',
data : JSON.stringify(moduleNames)
});
req.modules = modules;
req.moduleIndexToContainerId = containerIds;
req.done(this.processBackendSupportResults);
req.always(() => this.changeBackendSupportReadyState(true));
} else {
// All modules have already been checked once, show popup immediately
this.setupBackendCheckboxForm(modules);
this.changeBackendSupportReadyState(true);
}
};
Migrate.prototype.processBackendSupportResults = function(data, textStatus, req) {
let result = data.data;
$.each(result, (i, state) => {
let containerId = req.moduleIndexToContainerId[i];
this.knownBackendSupport[req.modules[containerId]] = state;
});
this.setupBackendCheckboxForm(req.modules);
};
Migrate.prototype.setupBackendCheckboxForm = function(modules) {
let supportedModules = {};
$.each(modules, (id, module) => {
if (this.knownBackendSupport[module]) {
supportedModules[id] = module;
}
});
if (Object.keys(supportedModules).length) {
let $form = this.Popup().find('.suggestion-area > #setAsBackendForm');
if (! $form.length) {
$form = $(BACKEND_FORM);
this.Popup().find('.suggestion-area > ul').after($form);
}
this.backendSupportRelated = { ...this.backendSupportRelated, ...supportedModules };
let req = $.ajax({
context : this,
type : 'get',
url : this.icinga.config.baseUrl + '/icingadb/migrate/checkbox-state'
});
req.done(this.setCheckboxState);
}
};
Migrate.prototype.setCheckboxState = function(isChecked, textStatus, req) {
var $form = this.Popup().find('.suggestion-area > #setAsBackendForm');
var $checkbox = $form.find('input#setAsBackendForm-checkbox');
this.Popup().find('p').hide();
$checkbox.prop('checked', isChecked);
this.showPopup();
}
Migrate.prototype.removeBackendCheckboxForm = function () {
var $backendCheckboxForm = this.Popup().find('.suggestion-area > #setAsBackendForm');
if ($backendCheckboxForm.length) {
$backendCheckboxForm.remove();
}
this.Popup().find('p').show();
}
Migrate.prototype.addSuggestions = function(urls) {
var _this = this,
hasSuggestions = false,
@ -368,9 +444,7 @@
Migrate.prototype.cleanupSuggestions = function() {
var _this = this,
toBeRemoved = [],
willBeEmpty = true,
$hasBackendForm = this.Popup().find('.suggestion-area > #setAsBackendForm').length > 0;
toBeRemoved = [];
this.Popup().find('li').each(function () {
var $suggestion = $(this);
var $container = $('#' + $suggestion.data('containerId'));
@ -384,26 +458,58 @@
|| containerUrl === _this.knownMigrations[containerUrl]
) {
toBeRemoved.push($suggestion);
} else {
willBeEmpty = false;
}
});
if ($hasBackendForm) {
willBeEmpty = false;
return toBeRemoved;
};
Migrate.prototype.cleanupBackendForm = function () {
let $form = this.Popup().find('#setAsBackendForm');
if (! $form.length) {
return false;
}
if (willBeEmpty) {
let stillRelated = {};
$.each(this.backendSupportRelated, (id, module) => {
let $container = $('#' + id);
if ($container.length && $container.data('icingaModule') === module) {
stillRelated[id] = module;
}
});
this.backendSupportRelated = stillRelated;
if (Object.keys(stillRelated).length) {
return true;
}
return $form;
};
Migrate.prototype.cleanupPopup = function () {
let toBeRemoved = this.cleanupSuggestions();
let hasBackendForm = this.cleanupBackendForm();
if (hasBackendForm !== true && this.Popup().find('li').length === toBeRemoved.length) {
this.hidePopup(function () {
// Let the transition finish first, looks cleaner
$.each(toBeRemoved, function (_, $suggestion) {
$suggestion.remove();
});
if (typeof hasBackendForm === 'object') {
hasBackendForm.remove();
}
});
} else {
$.each(toBeRemoved, function (_, $suggestion) {
$suggestion.remove();
});
if (typeof hasBackendForm === 'object') {
hasBackendForm.remove();
}
}
};
@ -450,6 +556,34 @@
}
};
Migrate.prototype.setUrlMigrationReadyState = function (state) {
this.urlMigrationReadyState = state;
};
Migrate.prototype.changeUrlMigrationReadyState = function (state) {
this.setUrlMigrationReadyState(state);
if (this.backendSupportReadyState !== false) {
this.backendSupportReadyState = null;
this.urlMigrationReadyState = null;
this.cleanupPopup();
}
};
Migrate.prototype.setBackendSupportReadyState = function (state) {
this.backendSupportReadyState = state;
};
Migrate.prototype.changeBackendSupportReadyState = function (state) {
this.setBackendSupportReadyState(state);
if (this.urlMigrationReadyState !== false) {
this.backendSupportReadyState = null;
this.urlMigrationReadyState = null;
this.cleanupPopup();
}
};
Migrate.prototype.Popup = function() {
// Node.contains() is used due to `?renderLayout`
if (this.$popup === null || ! document.body.contains(this.$popup[0])) {