new: [instance] Added first version of database migration plugin
parent
9a25d98c9a
commit
a8951ed69e
|
@ -704,6 +704,10 @@ class ACLComponent extends Component
|
||||||
'home' => [
|
'home' => [
|
||||||
'url' => '/instance/home',
|
'url' => '/instance/home',
|
||||||
'label' => __('Home')
|
'label' => __('Home')
|
||||||
|
],
|
||||||
|
'migration' => [
|
||||||
|
'url' => '/instance/migrationIndex',
|
||||||
|
'label' => __('Database migration')
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
|
@ -6,12 +6,18 @@ use App\Controller\AppController;
|
||||||
use Cake\Utility\Hash;
|
use Cake\Utility\Hash;
|
||||||
use Cake\Utility\Text;
|
use Cake\Utility\Text;
|
||||||
use \Cake\Database\Expression\QueryExpression;
|
use \Cake\Database\Expression\QueryExpression;
|
||||||
|
use Cake\Event\EventInterface;
|
||||||
|
|
||||||
class InstanceController extends AppController
|
class InstanceController extends AppController
|
||||||
{
|
{
|
||||||
|
public function beforeFilter(EventInterface $event)
|
||||||
|
{
|
||||||
|
parent::beforeFilter($event);
|
||||||
|
$this->set('metaGroup', !empty($this->isAdmin) ? 'Cerebrate' : 'Administration');
|
||||||
|
}
|
||||||
|
|
||||||
public function home()
|
public function home()
|
||||||
{
|
{
|
||||||
$this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate');
|
|
||||||
$this->set('md', file_get_contents(ROOT . '/README.md'));
|
$this->set('md', file_get_contents(ROOT . '/README.md'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,4 +28,77 @@ class InstanceController extends AppController
|
||||||
$data['user'] = $this->ACL->getUser();
|
$data['user'] = $this->ACL->getUser();
|
||||||
return $this->RestResponse->viewData($data, 'json');
|
return $this->RestResponse->viewData($data, 'json');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function migrationIndex()
|
||||||
|
{
|
||||||
|
$migrationStatus = $this->Instance->getMigrationStatus();
|
||||||
|
|
||||||
|
$this->loadModel('Phinxlog');
|
||||||
|
$status = $this->Phinxlog->mergeMigrationLogIntoStatus($migrationStatus['status']);
|
||||||
|
|
||||||
|
$this->set('status', $status);
|
||||||
|
$this->set('updateAvailables', $migrationStatus['updateAvailables']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function migrate($version=null) {
|
||||||
|
if ($this->request->is('post')) {
|
||||||
|
if (is_null($version)) {
|
||||||
|
$migrateResult = $this->Instance->migrate();
|
||||||
|
} else {
|
||||||
|
$migrateResult = $this->Instance->migrate(['target' => $version]);
|
||||||
|
}
|
||||||
|
if ($this->ParamHandler->isRest() || $this->ParamHandler->isAjax()) {
|
||||||
|
if ($migrateResult['success']) {
|
||||||
|
return $this->RestResponse->saveSuccessResponse('instance', 'migrate', false, false, __('Migration sucessful'));
|
||||||
|
} else {
|
||||||
|
return $this->RestResponse->saveFailResponse('instance', 'migrate', false, $migrateResult['error']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($migrateResult['success']) {
|
||||||
|
$this->Flash->success(__('Migration sucessful'));
|
||||||
|
$this->redirect(['action' => 'migrationIndex']);
|
||||||
|
} else {
|
||||||
|
$this->Flash->error(__('Migration fail'));
|
||||||
|
$this->redirect(['action' => 'migrationIndex']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$migrationStatus = $this->Instance->getMigrationStatus();
|
||||||
|
$this->set('title', __n('Run database update?', 'Run all database updates?', count($migrationStatus['updateAvailables'])));
|
||||||
|
$this->set('question', __('The process might take some time.'));
|
||||||
|
$this->set('actionName', __n('Run update', 'Run all updates', count($migrationStatus['updateAvailables'])));
|
||||||
|
$this->set('path', ['controller' => 'instance', 'action' => 'migrate']);
|
||||||
|
$this->render('/genericTemplates/confirm');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rollback($version=null) {
|
||||||
|
if ($this->request->is('post')) {
|
||||||
|
if (is_null($version)) {
|
||||||
|
$migrateResult = $this->Instance->rollback();
|
||||||
|
} else {
|
||||||
|
$migrateResult = $this->Instance->rollback(['target' => $version]);
|
||||||
|
}
|
||||||
|
if ($this->ParamHandler->isRest() || $this->ParamHandler->isAjax()) {
|
||||||
|
if ($migrateResult['success']) {
|
||||||
|
return $this->RestResponse->saveSuccessResponse('instance', 'rollback', false, false, __('Rollback sucessful'));
|
||||||
|
} else {
|
||||||
|
return $this->RestResponse->saveFailResponse('instance', 'rollback', false, $migrateResult['error']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($migrateResult['success']) {
|
||||||
|
$this->Flash->success(__('Rollback sucessful'));
|
||||||
|
$this->redirect(['action' => 'migrationIndex']);
|
||||||
|
} else {
|
||||||
|
$this->Flash->error(__('Rollback fail'));
|
||||||
|
$this->redirect(['action' => 'migrationIndex']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$migrationStatus = $this->Instance->getMigrationStatus();
|
||||||
|
$this->set('title', __('Run database rollback?'));
|
||||||
|
$this->set('question', __('The process might take some time.'));
|
||||||
|
$this->set('actionName', __('Run rollback'));
|
||||||
|
$this->set('path', ['controller' => 'instance', 'action' => 'rollback']);
|
||||||
|
$this->render('/genericTemplates/confirm');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ namespace App\Model\Table;
|
||||||
use App\Model\Table\AppTable;
|
use App\Model\Table\AppTable;
|
||||||
use Cake\ORM\Table;
|
use Cake\ORM\Table;
|
||||||
use Cake\Validation\Validator;
|
use Cake\Validation\Validator;
|
||||||
|
use Migrations\Migrations;
|
||||||
|
|
||||||
class InstanceTable extends AppTable
|
class InstanceTable extends AppTable
|
||||||
{
|
{
|
||||||
|
@ -17,4 +18,43 @@ class InstanceTable extends AppTable
|
||||||
{
|
{
|
||||||
return $validator;
|
return $validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getMigrationStatus()
|
||||||
|
{
|
||||||
|
$migrations = new Migrations();
|
||||||
|
$status = $migrations->status();
|
||||||
|
$status = array_reverse($status);
|
||||||
|
|
||||||
|
$updateAvailables = array_filter($status, function ($update) {
|
||||||
|
return $update['status'] != 'up';
|
||||||
|
});
|
||||||
|
return [
|
||||||
|
'status' => $status,
|
||||||
|
'updateAvailables' => $updateAvailables,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function migrate($version=null) {
|
||||||
|
$migrations = new Migrations();
|
||||||
|
if (is_null($version)) {
|
||||||
|
$migrationResult = $migrations->migrate();
|
||||||
|
} else {
|
||||||
|
$migrationResult = $migrations->migrate(['target' => $version]);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'success' => true
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rollback($version=null) {
|
||||||
|
$migrations = new Migrations();
|
||||||
|
if (is_null($version)) {
|
||||||
|
$migrationResult = $migrations->rollback();
|
||||||
|
} else {
|
||||||
|
$migrationResult = $migrations->rollback(['target' => $version]);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'success' => true
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Model\Table;
|
||||||
|
|
||||||
|
use App\Model\Table\AppTable;
|
||||||
|
use Cake\ORM\Table;
|
||||||
|
|
||||||
|
class PhinxlogTable extends AppTable
|
||||||
|
{
|
||||||
|
public function initialize(array $config): void
|
||||||
|
{
|
||||||
|
parent::initialize($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mergeMigrationLogIntoStatus(array $status): array
|
||||||
|
{
|
||||||
|
$logs = $this->find('list', [
|
||||||
|
'keyField' => 'version',
|
||||||
|
'valueField' => function ($entry) {
|
||||||
|
return $entry;
|
||||||
|
}
|
||||||
|
])->toArray();
|
||||||
|
foreach ($status as &$entry) {
|
||||||
|
if (!empty($logs[$entry['id']])) {
|
||||||
|
$logEntry = $logs[$entry['id']];
|
||||||
|
$startTime = $logEntry['start_time'];
|
||||||
|
$startTime->setToStringFormat('yyyy-MM-dd HH:mm:ss');
|
||||||
|
$endTime = $logEntry['end_time'];
|
||||||
|
$endTime->setToStringFormat('yyyy-MM-dd HH:mm:ss');
|
||||||
|
$timeTaken = $logEntry['end_time']->diff($logEntry['start_time']);
|
||||||
|
$timeTakenFormated = sprintf('%s min %s sec',
|
||||||
|
floor(abs($logEntry['end_time']->getTimestamp() - $logEntry['start_time']->getTimestamp()) / 60),
|
||||||
|
abs($logEntry['end_time']->getTimestamp() - $logEntry['start_time']->getTimestamp()) % 60
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$startTime = 'N/A';
|
||||||
|
$endTime = 'N/A';
|
||||||
|
$timeTaken = 'N/A';
|
||||||
|
$timeTakenFormated = 'N/A';
|
||||||
|
}
|
||||||
|
$entry['start_time'] = $startTime;
|
||||||
|
$entry['end_time'] = $endTime;
|
||||||
|
$entry['time_taken'] = $timeTaken;
|
||||||
|
$entry['time_taken_formated'] = $timeTakenFormated;
|
||||||
|
}
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
if (!empty($updateAvailables)) {
|
||||||
|
$alertHtml = sprintf(
|
||||||
|
'<h5 class="alert-heading">%s</h5>%s<div>%s</div>',
|
||||||
|
__n('A new update is available!', 'New updates are available!', count($updateAvailables)),
|
||||||
|
__('Updating to the latest version is highly recommanded.'),
|
||||||
|
$this->Bootstrap->button([
|
||||||
|
'variant' => 'success',
|
||||||
|
'icon' => 'arrow-alt-circle-up',
|
||||||
|
'class' => 'mt-1',
|
||||||
|
'text' => __n('Run update', 'Run all updates', count($updateAvailables)),
|
||||||
|
'params' => [
|
||||||
|
'onclick' => 'runAllUpdate()'
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
echo $this->Bootstrap->alert([
|
||||||
|
'variant' => 'warning',
|
||||||
|
'html' => $alertHtml,
|
||||||
|
'dismissible' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($status as $i => &$update) {
|
||||||
|
if ($update['status'] == 'up') {
|
||||||
|
$update['_rowVariant'] = 'success';
|
||||||
|
} else if ($update['status'] == 'down') {
|
||||||
|
$update['_rowVariant'] = 'danger';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $this->Bootstrap->table([], [
|
||||||
|
'fields' => [
|
||||||
|
['key' => 'id', 'label' => __('ID')],
|
||||||
|
['key' => 'name', 'label' => __('Name')],
|
||||||
|
['key' => 'end_time', 'label' => __('End Time')],
|
||||||
|
['key' => 'time_taken_formated', 'label' => __('Time Taken')],
|
||||||
|
['key' => 'status', 'label' => __('Status')]
|
||||||
|
],
|
||||||
|
'items' => $status,
|
||||||
|
]);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function runAllUpdate() {
|
||||||
|
const url = '/instance/migrate'
|
||||||
|
const reloadUrl = '/instance/migrate-index'
|
||||||
|
const modalOptions = {
|
||||||
|
title: '<?= __n('Run database update?', 'Run all database updates?', count($updateAvailables)) ?>',
|
||||||
|
body: '<?= __('The process might take some time.') ?>',
|
||||||
|
type: 'confirm-success',
|
||||||
|
confirmText: '<?= __n('Run update', 'Run all updates', count($updateAvailables)) ?>',
|
||||||
|
APIConfirm: (tmpApi) => {
|
||||||
|
tmpApi.fetchAndPostForm(url, {}).then(() => {
|
||||||
|
location.reload()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
UI.modal(modalOptions)
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -142,7 +142,7 @@ class AJAXApi {
|
||||||
static async quickFetchAndPostForm(url, dataToMerge={}, options={}) {
|
static async quickFetchAndPostForm(url, dataToMerge={}, options={}) {
|
||||||
const constAlteredOptions = Object.assign({}, {}, options)
|
const constAlteredOptions = Object.assign({}, {}, options)
|
||||||
const tmpApi = new AJAXApi(constAlteredOptions)
|
const tmpApi = new AJAXApi(constAlteredOptions)
|
||||||
return tmpApi.fetchAndPostForm(url, dataToMerge, constAlteredOptions.skipRequestHooks)
|
return tmpApi.fetchAndPostForm(url, dataToMerge, constAlteredOptions.skipRequestHooks, constAlteredOptions.skipFeedback)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -335,14 +335,14 @@ class AJAXApi {
|
||||||
* @param {boolean} [skipRequestHooks=false] - If true, default request hooks will be skipped
|
* @param {boolean} [skipRequestHooks=false] - If true, default request hooks will be skipped
|
||||||
* @return {Promise<Object>} Promise object resolving to the result of the POST operation
|
* @return {Promise<Object>} Promise object resolving to the result of the POST operation
|
||||||
*/
|
*/
|
||||||
async fetchAndPostForm(url, dataToMerge={}, skipRequestHooks=false) {
|
async fetchAndPostForm(url, dataToMerge={}, skipRequestHooks=false, skipFeedback=false) {
|
||||||
if (!skipRequestHooks) {
|
if (!skipRequestHooks) {
|
||||||
this.beforeRequest()
|
this.beforeRequest()
|
||||||
}
|
}
|
||||||
let toReturn
|
let toReturn
|
||||||
try {
|
try {
|
||||||
const form = await this.fetchForm(url, true, true);
|
const form = await this.fetchForm(url, true, true);
|
||||||
toReturn = await this.postForm(form, dataToMerge, true, true)
|
toReturn = await this.postForm(form, dataToMerge, true, skipFeedback)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toReturn = Promise.reject(error);
|
toReturn = Promise.reject(error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
Loading…
Reference in New Issue