new: [instance] Added first version of database migration plugin
parent
9a25d98c9a
commit
a8951ed69e
|
@ -704,6 +704,10 @@ class ACLComponent extends Component
|
|||
'home' => [
|
||||
'url' => '/instance/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\Text;
|
||||
use \Cake\Database\Expression\QueryExpression;
|
||||
use Cake\Event\EventInterface;
|
||||
|
||||
class InstanceController extends AppController
|
||||
{
|
||||
public function beforeFilter(EventInterface $event)
|
||||
{
|
||||
parent::beforeFilter($event);
|
||||
$this->set('metaGroup', !empty($this->isAdmin) ? 'Cerebrate' : 'Administration');
|
||||
}
|
||||
|
||||
public function home()
|
||||
{
|
||||
$this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate');
|
||||
$this->set('md', file_get_contents(ROOT . '/README.md'));
|
||||
}
|
||||
|
||||
|
@ -22,4 +28,77 @@ class InstanceController extends AppController
|
|||
$data['user'] = $this->ACL->getUser();
|
||||
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 Cake\ORM\Table;
|
||||
use Cake\Validation\Validator;
|
||||
use Migrations\Migrations;
|
||||
|
||||
class InstanceTable extends AppTable
|
||||
{
|
||||
|
@ -17,4 +18,43 @@ class InstanceTable extends AppTable
|
|||
{
|
||||
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={}) {
|
||||
const constAlteredOptions = Object.assign({}, {}, options)
|
||||
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
|
||||
* @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) {
|
||||
this.beforeRequest()
|
||||
}
|
||||
let toReturn
|
||||
try {
|
||||
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) {
|
||||
toReturn = Promise.reject(error);
|
||||
} finally {
|
||||
|
|
Loading…
Reference in New Issue