From a8951ed69eca99e2a1249782ea26a27e0b43b812 Mon Sep 17 00:00:00 2001 From: mokaddem Date: Wed, 24 Feb 2021 11:05:23 +0100 Subject: [PATCH] new: [instance] Added first version of database migration plugin --- src/Controller/Component/ACLComponent.php | 4 ++ src/Controller/InstanceController.php | 81 ++++++++++++++++++++++- src/Model/Table/InstanceTable.php | 40 +++++++++++ src/Model/Table/PhinxlogTable.php | 48 ++++++++++++++ templates/Instance/migration_index.php | 61 +++++++++++++++++ webroot/js/api-helper.js | 6 +- 6 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 src/Model/Table/PhinxlogTable.php create mode 100644 templates/Instance/migration_index.php diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index 9917fe6..e7ff59f 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -704,6 +704,10 @@ class ACLComponent extends Component 'home' => [ 'url' => '/instance/home', 'label' => __('Home') + ], + 'migration' => [ + 'url' => '/instance/migrationIndex', + 'label' => __('Database migration') ] ] ], diff --git a/src/Controller/InstanceController.php b/src/Controller/InstanceController.php index e88fa07..df141a9 100644 --- a/src/Controller/InstanceController.php +++ b/src/Controller/InstanceController.php @@ -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'); + } } diff --git a/src/Model/Table/InstanceTable.php b/src/Model/Table/InstanceTable.php index 91e38fe..5613e95 100644 --- a/src/Model/Table/InstanceTable.php +++ b/src/Model/Table/InstanceTable.php @@ -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 + ]; + } } diff --git a/src/Model/Table/PhinxlogTable.php b/src/Model/Table/PhinxlogTable.php new file mode 100644 index 0000000..36d9222 --- /dev/null +++ b/src/Model/Table/PhinxlogTable.php @@ -0,0 +1,48 @@ +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; + } +} diff --git a/templates/Instance/migration_index.php b/templates/Instance/migration_index.php new file mode 100644 index 0000000..b554dd1 --- /dev/null +++ b/templates/Instance/migration_index.php @@ -0,0 +1,61 @@ +%s%s
%s
', + __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, +]); +?> + + \ No newline at end of file diff --git a/webroot/js/api-helper.js b/webroot/js/api-helper.js index 8987afb..282b1b4 100644 --- a/webroot/js/api-helper.js +++ b/webroot/js/api-helper.js @@ -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} 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 {