Introduce `MigrationsController` & add pending migrations list in about view

This commit is contained in:
Yonas Habteab 2023-07-24 13:59:19 +02:00 committed by Johannes Meyer
parent a9db85ed71
commit 1da5487066
5 changed files with 316 additions and 11 deletions

View 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();
}
}

View File

@ -1,7 +1,10 @@
<?php
use Icinga\Application\MigrationManager;
use Icinga\Web\Navigation\Renderer\BadgeNavigationItemRenderer;
use ipl\Html\HtmlElement;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\StateBadge;
?>
<div class="controls">
@ -93,6 +96,31 @@ use ipl\Web\Widget\Icon;
</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>
<table class="name-value-table" data-base-target="_next">
<?php foreach ($libraries as $library): ?>

View File

@ -6,7 +6,9 @@ namespace Icinga\Web\Navigation;
use Icinga\Application\Hook\HealthHook;
use Icinga\Application\Icinga;
use Icinga\Application\Logger;
use Icinga\Application\MigrationManager;
use Icinga\Authentication\Auth;
use Icinga\Web\Navigation\Renderer\BadgeNavigationItemRenderer;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
@ -47,6 +49,10 @@ class ConfigMenu extends BaseHtmlElement
'label' => t('Health'),
'url' => 'health',
],
'migrations' => [
'label' => t('Migrations'),
'url' => 'migrations',
],
'announcements' => [
'label' => t('Announcements'),
'url' => 'announcements'
@ -136,7 +142,7 @@ class ConfigMenu extends BaseHtmlElement
null,
[
new Icon('cog'),
$this->createHealthBadge(),
$this->createHealthBadge() ?? $this->createMigrationBadge(),
]
),
$this->createLevel2Menu()
@ -211,7 +217,7 @@ class ConfigMenu extends BaseHtmlElement
return false;
}
protected function createHealthBadge()
protected function createHealthBadge(): ?StateBadge
{
$stateBadge = null;
if ($this->getHealthCount() > 0) {
@ -222,6 +228,20 @@ class ConfigMenu extends BaseHtmlElement
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()
{
$level2Nav = HtmlElement::create(
@ -240,23 +260,26 @@ class ConfigMenu extends BaseHtmlElement
return null;
}
$healthBadge = null;
$stateBadge = null;
$class = null;
if ($key === 'health') {
$class = 'badge-nav-item';
$healthBadge = $this->createHealthBadge();
$stateBadge = $this->createHealthBadge();
} elseif ($key === 'migrations') {
$class = 'badge-nav-item';
$stateBadge = $this->createMigrationBadge();
}
$li = HtmlElement::create(
'li',
isset($item['atts']) ? $item['atts'] : [],
$item['atts'] ?? [],
[
HtmlElement::create(
'a',
Attributes::create(['href' => Url::fromPath($item['url'])]),
[
$item['label'],
isset($healthBadge) ? $healthBadge : ''
$stateBadge ?? ''
]
),
]

View File

@ -13165,11 +13165,6 @@ parameters:
count: 1
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\\.$#"
count: 1

View File

@ -13,6 +13,20 @@
> * {
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 {