mirror of
https://github.com/Icinga/icingaweb2.git
synced 2025-07-23 13:54:26 +02:00
Introduce MigrationsController
& add pending migrations list in about view
This commit is contained in:
parent
a9db85ed71
commit
1da5487066
245
application/controllers/MigrationsController.php
Normal file
245
application/controllers/MigrationsController.php
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Controllers;
|
||||||
|
|
||||||
|
use Icinga\Application\Hook\MigrationHook;
|
||||||
|
use Icinga\Application\MigrationManager;
|
||||||
|
use Icinga\Exception\MissingParameterException;
|
||||||
|
use Icinga\Forms\MigrationForm;
|
||||||
|
use Icinga\Web\Notification;
|
||||||
|
use Icinga\Web\Widget\ItemList\MigrationList;
|
||||||
|
use Icinga\Web\Widget\Tabextension\OutputFormat;
|
||||||
|
use ipl\Html\Attributes;
|
||||||
|
use ipl\Html\HtmlElement;
|
||||||
|
use ipl\Html\Text;
|
||||||
|
use ipl\Web\Compat\CompatController;
|
||||||
|
use ipl\Web\Widget\ActionLink;
|
||||||
|
|
||||||
|
class MigrationsController extends CompatController
|
||||||
|
{
|
||||||
|
public function indexAction(): void
|
||||||
|
{
|
||||||
|
$mm = MigrationManager::instance();
|
||||||
|
|
||||||
|
$this->getTabs()->extend(new OutputFormat(['csv']));
|
||||||
|
$this->addTitleTab($this->translate('Migrations'));
|
||||||
|
|
||||||
|
$canApply = $this->hasPermission('application/migrations');
|
||||||
|
if (! $canApply) {
|
||||||
|
$this->addControl(
|
||||||
|
new HtmlElement(
|
||||||
|
'div',
|
||||||
|
Attributes::create(['class' => 'migration-state-banner']),
|
||||||
|
new HtmlElement(
|
||||||
|
'span',
|
||||||
|
null,
|
||||||
|
Text::create(
|
||||||
|
$this->translate('You do not have the required permission to apply pending migrations.')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$migrateListForm = new MigrationForm();
|
||||||
|
$migrateGlobalForm = new MigrationForm();
|
||||||
|
$migrateGlobalForm->getAttributes()->set('name', sprintf('migrate-%s', MigrationHook::ALL_MIGRATIONS));
|
||||||
|
|
||||||
|
if ($canApply && $mm->hasPendingMigrations()) {
|
||||||
|
$migrateGlobalForm->addElement('submit', sprintf('migrate-%s', MigrationHook::ALL_MIGRATIONS), [
|
||||||
|
'required' => true,
|
||||||
|
'label' => $this->translate('Migrate All'),
|
||||||
|
'title' => $this->translate('Migrate all pending migrations')
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->controls->getAttributes()->add('class', 'default-layout');
|
||||||
|
$this->handleMigrateRequest($migrateGlobalForm);
|
||||||
|
|
||||||
|
$this->addControl($migrateGlobalForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->handleFormatRequest($mm->toArray());
|
||||||
|
|
||||||
|
$frameworkList = new MigrationList($mm->yieldMigrations(), $migrateListForm);
|
||||||
|
$frameworkListControl = new HtmlElement('div', Attributes::create(['class' => 'migration-list-control']));
|
||||||
|
$frameworkListControl->addHtml(new HtmlElement('h2', null, Text::create($this->translate('System'))));
|
||||||
|
$frameworkListControl->addHtml($frameworkList);
|
||||||
|
|
||||||
|
$moduleList = new MigrationList($mm->yieldMigrations(true), $migrateListForm);
|
||||||
|
$moduleListControl = new HtmlElement('div', Attributes::create(['class' => 'migration-list-control']));
|
||||||
|
$moduleListControl->addHtml(new HtmlElement('h2', null, Text::create($this->translate('Modules'))));
|
||||||
|
$moduleListControl->addHtml($moduleList);
|
||||||
|
|
||||||
|
$migrateListForm->addHtml($frameworkListControl, $moduleListControl);
|
||||||
|
if ($canApply && $mm->hasPendingMigrations()) {
|
||||||
|
$frameworkList->ensureAssembled();
|
||||||
|
$moduleList->ensureAssembled();
|
||||||
|
|
||||||
|
$this->handleMigrateRequest($migrateListForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
$migrations = new HtmlElement('div', Attributes::create(['class' => 'migrations']));
|
||||||
|
$migrations->addHtml($migrateListForm);
|
||||||
|
|
||||||
|
$this->addContent($migrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hintAction(): void
|
||||||
|
{
|
||||||
|
// The forwarded request doesn't modify the original server query string, but adds the migration param to the
|
||||||
|
// request param instead. So, there is no way to access the migration param other than via the request instance.
|
||||||
|
/** @var ?string $module */
|
||||||
|
$module = $this->getRequest()->getParam(MigrationHook::MIGRATION_PARAM);
|
||||||
|
if ($module === null) {
|
||||||
|
throw new MissingParameterException(t('Required parameter \'%s\' missing'), MigrationHook::MIGRATION_PARAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mm = MigrationManager::instance();
|
||||||
|
if (! $mm->hasMigrations($module)) {
|
||||||
|
$this->httpNotFound(sprintf('There are no pending migrations matching the given name: %s', $module));
|
||||||
|
}
|
||||||
|
|
||||||
|
$migration = $mm->getMigration($module);
|
||||||
|
$this->addTitleTab($this->translate('Error'));
|
||||||
|
$this->addContent(
|
||||||
|
new HtmlElement(
|
||||||
|
'div',
|
||||||
|
Attributes::create(['class' => 'pending-migrations-hint']),
|
||||||
|
new HtmlElement('h2', null, Text::create($this->translate('Error!'))),
|
||||||
|
new HtmlElement(
|
||||||
|
'p',
|
||||||
|
null,
|
||||||
|
Text::create(sprintf($this->translate('%s has pending migrations.'), $migration->getName()))
|
||||||
|
),
|
||||||
|
new HtmlElement('p', null, Text::create($this->translate('Please apply the migrations first.'))),
|
||||||
|
new ActionLink($this->translate('View pending Migrations'), 'migrations')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function migrationAction(): void
|
||||||
|
{
|
||||||
|
/** @var string $name */
|
||||||
|
$name = $this->params->getRequired(MigrationHook::MIGRATION_PARAM);
|
||||||
|
|
||||||
|
$this->addTitleTab($this->translate('Migration'));
|
||||||
|
$this->getTabs()->disableLegacyExtensions();
|
||||||
|
$this->controls->getAttributes()->add('class', 'default-layout');
|
||||||
|
|
||||||
|
$mm = MigrationManager::instance();
|
||||||
|
if (! $mm->hasMigrations($name)) {
|
||||||
|
$migrations = [];
|
||||||
|
} else {
|
||||||
|
$hook = $mm->getMigration($name);
|
||||||
|
$migrations = array_reverse($hook->getMigrations());
|
||||||
|
if (! $this->hasPermission('application/migrations')) {
|
||||||
|
$this->addControl(
|
||||||
|
new HtmlElement(
|
||||||
|
'div',
|
||||||
|
Attributes::create(['class' => 'migration-state-banner']),
|
||||||
|
new HtmlElement(
|
||||||
|
'span',
|
||||||
|
null,
|
||||||
|
Text::create(
|
||||||
|
$this->translate('You do not have the required permission to apply pending migrations.')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$migrateForm = (new MigrationForm())
|
||||||
|
->addElement(
|
||||||
|
'submit',
|
||||||
|
sprintf('migrate-%s', $hook->getModuleName()),
|
||||||
|
[
|
||||||
|
'required' => true,
|
||||||
|
'label' => $this->translate('Migrate'),
|
||||||
|
'title' => sprintf(
|
||||||
|
$this->translatePlural(
|
||||||
|
'Migrate %d pending migration',
|
||||||
|
'Migrate all %d pending migrations',
|
||||||
|
$hook->count()
|
||||||
|
),
|
||||||
|
$hook->count()
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$migrateForm->getAttributes()->add('class', 'inline');
|
||||||
|
$this->handleMigrateRequest($migrateForm);
|
||||||
|
|
||||||
|
$this->addControl(
|
||||||
|
new HtmlElement(
|
||||||
|
'div',
|
||||||
|
Attributes::create(['class' => 'migration-controls']),
|
||||||
|
new HtmlElement('span', null, Text::create($hook->getName())),
|
||||||
|
$migrateForm
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$migrationWidget = new HtmlElement('div', Attributes::create(['class' => 'migrations']));
|
||||||
|
$migrationWidget->addHtml((new MigrationList($migrations))->setMinimal(false));
|
||||||
|
$this->addContent($migrationWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleMigrateRequest(MigrationForm $form): void
|
||||||
|
{
|
||||||
|
$this->assertPermission('application/migrations');
|
||||||
|
|
||||||
|
$form->on(MigrationForm::ON_SUCCESS, function (MigrationForm $form) {
|
||||||
|
$mm = MigrationManager::instance();
|
||||||
|
|
||||||
|
$pressedButton = $form->getPressedSubmitElement();
|
||||||
|
if ($pressedButton) {
|
||||||
|
$name = substr($pressedButton->getName(), 8);
|
||||||
|
switch ($name) {
|
||||||
|
case MigrationHook::ALL_MIGRATIONS:
|
||||||
|
if ($mm->applyAll()) {
|
||||||
|
Notification::success($this->translate('Applied all migrations successfully'));
|
||||||
|
} else {
|
||||||
|
Notification::error(
|
||||||
|
$this->translate(
|
||||||
|
'Applied migrations successfully. Though, one or more migration hooks failed to run.'
|
||||||
|
. ' See logs for details'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$migration = $mm->getMigration($name);
|
||||||
|
if ($mm->apply($migration)) {
|
||||||
|
Notification::success($this->translate('Applied pending migrations successfully'));
|
||||||
|
} else {
|
||||||
|
Notification::error(
|
||||||
|
$this->translate('Failed to apply pending migration(s). See logs for details')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->redirectNow('migrations');
|
||||||
|
})->handleRequest($this->getServerRequest());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle exports
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
*/
|
||||||
|
protected function handleFormatRequest(array $data): void
|
||||||
|
{
|
||||||
|
$formatJson = $this->params->get('format') === 'json';
|
||||||
|
if (! $formatJson && ! $this->getRequest()->isApiRequest()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getResponse()
|
||||||
|
->json()
|
||||||
|
->setSuccessData($data)
|
||||||
|
->sendResponse();
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Icinga\Application\MigrationManager;
|
||||||
|
use Icinga\Web\Navigation\Renderer\BadgeNavigationItemRenderer;
|
||||||
use ipl\Html\HtmlElement;
|
use ipl\Html\HtmlElement;
|
||||||
use ipl\Web\Widget\Icon;
|
use ipl\Web\Widget\Icon;
|
||||||
|
use ipl\Web\Widget\StateBadge;
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
@ -93,6 +96,31 @@ use ipl\Web\Widget\Icon;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php $mm = MigrationManager::instance(); if ($mm->hasPendingMigrations()): ?>
|
||||||
|
<div class="pending-migrations clearfix">
|
||||||
|
<h2><?= $this->translate('Pending Migrations') ?></h2>
|
||||||
|
<table class="name-value-table migrations">
|
||||||
|
<?php foreach ($mm->getPendingMigrations() as $migration): ?>
|
||||||
|
<tr>
|
||||||
|
<th><?= $this->escape($migration->getName()) ?></th>
|
||||||
|
<td><?=
|
||||||
|
new StateBadge(
|
||||||
|
count($migration->getMigrations()),
|
||||||
|
BadgeNavigationItemRenderer::STATE_PENDING
|
||||||
|
);
|
||||||
|
?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach ?>
|
||||||
|
</table>
|
||||||
|
<?= $this->qlink(
|
||||||
|
$this->translate('Show all'),
|
||||||
|
'migrations',
|
||||||
|
null,
|
||||||
|
['title' => $this->translate('Show all pending migrations')]
|
||||||
|
) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif ?>
|
||||||
|
|
||||||
<h2><?= $this->translate('Loaded Libraries') ?></h2>
|
<h2><?= $this->translate('Loaded Libraries') ?></h2>
|
||||||
<table class="name-value-table" data-base-target="_next">
|
<table class="name-value-table" data-base-target="_next">
|
||||||
<?php foreach ($libraries as $library): ?>
|
<?php foreach ($libraries as $library): ?>
|
||||||
|
@ -6,7 +6,9 @@ namespace Icinga\Web\Navigation;
|
|||||||
use Icinga\Application\Hook\HealthHook;
|
use Icinga\Application\Hook\HealthHook;
|
||||||
use Icinga\Application\Icinga;
|
use Icinga\Application\Icinga;
|
||||||
use Icinga\Application\Logger;
|
use Icinga\Application\Logger;
|
||||||
|
use Icinga\Application\MigrationManager;
|
||||||
use Icinga\Authentication\Auth;
|
use Icinga\Authentication\Auth;
|
||||||
|
use Icinga\Web\Navigation\Renderer\BadgeNavigationItemRenderer;
|
||||||
use ipl\Html\Attributes;
|
use ipl\Html\Attributes;
|
||||||
use ipl\Html\BaseHtmlElement;
|
use ipl\Html\BaseHtmlElement;
|
||||||
use ipl\Html\HtmlElement;
|
use ipl\Html\HtmlElement;
|
||||||
@ -47,6 +49,10 @@ class ConfigMenu extends BaseHtmlElement
|
|||||||
'label' => t('Health'),
|
'label' => t('Health'),
|
||||||
'url' => 'health',
|
'url' => 'health',
|
||||||
],
|
],
|
||||||
|
'migrations' => [
|
||||||
|
'label' => t('Migrations'),
|
||||||
|
'url' => 'migrations',
|
||||||
|
],
|
||||||
'announcements' => [
|
'announcements' => [
|
||||||
'label' => t('Announcements'),
|
'label' => t('Announcements'),
|
||||||
'url' => 'announcements'
|
'url' => 'announcements'
|
||||||
@ -136,7 +142,7 @@ class ConfigMenu extends BaseHtmlElement
|
|||||||
null,
|
null,
|
||||||
[
|
[
|
||||||
new Icon('cog'),
|
new Icon('cog'),
|
||||||
$this->createHealthBadge(),
|
$this->createHealthBadge() ?? $this->createMigrationBadge(),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
$this->createLevel2Menu()
|
$this->createLevel2Menu()
|
||||||
@ -211,7 +217,7 @@ class ConfigMenu extends BaseHtmlElement
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createHealthBadge()
|
protected function createHealthBadge(): ?StateBadge
|
||||||
{
|
{
|
||||||
$stateBadge = null;
|
$stateBadge = null;
|
||||||
if ($this->getHealthCount() > 0) {
|
if ($this->getHealthCount() > 0) {
|
||||||
@ -222,6 +228,20 @@ class ConfigMenu extends BaseHtmlElement
|
|||||||
return $stateBadge;
|
return $stateBadge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function createMigrationBadge(): ?StateBadge
|
||||||
|
{
|
||||||
|
$mm = MigrationManager::instance();
|
||||||
|
$count = $mm->count();
|
||||||
|
|
||||||
|
$stateBadge = null;
|
||||||
|
if ($count > 0) {
|
||||||
|
$stateBadge = new StateBadge($count, BadgeNavigationItemRenderer::STATE_PENDING);
|
||||||
|
$stateBadge->addAttributes(['class' => 'disabled']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stateBadge;
|
||||||
|
}
|
||||||
|
|
||||||
protected function createLevel2Menu()
|
protected function createLevel2Menu()
|
||||||
{
|
{
|
||||||
$level2Nav = HtmlElement::create(
|
$level2Nav = HtmlElement::create(
|
||||||
@ -240,23 +260,26 @@ class ConfigMenu extends BaseHtmlElement
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$healthBadge = null;
|
$stateBadge = null;
|
||||||
$class = null;
|
$class = null;
|
||||||
if ($key === 'health') {
|
if ($key === 'health') {
|
||||||
$class = 'badge-nav-item';
|
$class = 'badge-nav-item';
|
||||||
$healthBadge = $this->createHealthBadge();
|
$stateBadge = $this->createHealthBadge();
|
||||||
|
} elseif ($key === 'migrations') {
|
||||||
|
$class = 'badge-nav-item';
|
||||||
|
$stateBadge = $this->createMigrationBadge();
|
||||||
}
|
}
|
||||||
|
|
||||||
$li = HtmlElement::create(
|
$li = HtmlElement::create(
|
||||||
'li',
|
'li',
|
||||||
isset($item['atts']) ? $item['atts'] : [],
|
$item['atts'] ?? [],
|
||||||
[
|
[
|
||||||
HtmlElement::create(
|
HtmlElement::create(
|
||||||
'a',
|
'a',
|
||||||
Attributes::create(['href' => Url::fromPath($item['url'])]),
|
Attributes::create(['href' => Url::fromPath($item['url'])]),
|
||||||
[
|
[
|
||||||
$item['label'],
|
$item['label'],
|
||||||
isset($healthBadge) ? $healthBadge : ''
|
$stateBadge ?? ''
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -13165,11 +13165,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: library/Icinga/Web/Navigation/ConfigMenu.php
|
path: library/Icinga/Web/Navigation/ConfigMenu.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:createHealthBadge\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: library/Icinga/Web/Navigation/ConfigMenu.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:createLevel2Menu\\(\\) has no return type specified\\.$#"
|
message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:createLevel2Menu\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
@ -13,6 +13,20 @@
|
|||||||
> * {
|
> * {
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pending-migrations {
|
||||||
|
.name-value-table.migrations {
|
||||||
|
tr:not(:first-child):not(:last-child) {
|
||||||
|
border-top: 1px solid @gray-lighter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
float: right;
|
||||||
|
margin-top: 1em;
|
||||||
|
color: @icinga-blue
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user