Merge branch 'MDL-62487' of https://github.com/timhunt/moodle
[moodle.git] / backup / util / ui / amd / src / async_backup.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * This module updates the UI during an asynchronous
18  * backup or restore process.
19  *
20  * @module     backup/util/async_backup
21  * @package    core
22  * @copyright  2018 Matt Porritt <mattp@catalyst-au.net>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  * @since      3.7
25  */
26 define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates'],
27         function($, ajax, Str, notification, Templates) {
29     /**
30      * Module level constants.
31      *
32      * Using var instead of const as ES6 isn't fully supported yet.
33      */
34     var STATUS_EXECUTING = 800;
35     var STATUS_FINISHED_ERR = 900;
36     var STATUS_FINISHED_OK = 1000;
38     /**
39      * Module level variables.
40      */
41     var Asyncbackup = {};
42     var checkdelayoriginal = 15000; // This is the default time to use.
43     var checkdelay = 15000; // How often we should check for progress updates.
44     var checkdelaymultipler = 1.5; // If a request fails this multiplier will be used to increase the checkdelay value
45     var backupid; //  The backup id to get the progress for.
46     var contextid; //  The course this backup progress is for.
47     var restoreurl; //  The URL to view course restores.
48     var typeid; //  The type of operation backup or restore.
49     var backupintervalid; //  The id of the setInterval function.
50     var allbackupintervalid; //  The id of the setInterval function.
51     var allcopyintervalid; //  The id of the setInterval function.
52     var timeout = 2000; // Timeout for ajax requests.
54     /**
55      * Helper function to update UI components.
56      *
57      * @param {string} backupid The id to match elements on.
58      * @param {string} type The type of operation, backup or restore.
59      * @param {number} percentage The completion percentage to apply.
60      */
61     function updateElement(backupid, type, percentage) {
62         var percentagewidth = Math.round(percentage) + '%';
63         var elementbar = document.querySelectorAll("[data-" + type + "id=" + CSS.escape(backupid) + "]")[0];
64         var percentagetext = percentage.toFixed(2) + '%';
66         // Set progress bar percentage indicators
67         elementbar.setAttribute('aria-valuenow', percentagewidth);
68         elementbar.style.width = percentagewidth;
69         elementbar.innerHTML = percentagetext;
70     }
72     /**
73      * Updates the interval we use to check for backup progress.
74      *
75      * @param {Number} intervalid The id of the interval
76      * @param {Function} callback The function to use in setInterval
77      * @param {Number} value The specified interval (in milliseconds)
78      * @returns {Number}
79      */
80     function updateInterval(intervalid, callback, value) {
81         clearInterval(intervalid);
82         return setInterval(callback, value);
83     }
85     /**
86      * Update backup table row when an async backup completes.
87      *
88      * @param {string} backupid The id to match elements on.
89      */
90     function updateBackupTableRow(backupid) {
91         var statuscell = $('#' + backupid + '_bar').parent().parent();
92         var tablerow = statuscell.parent();
93         var cellsiblings = statuscell.siblings();
94         var timecell = cellsiblings[1];
95         var timevalue = $(timecell).text();
96         var filenamecell = cellsiblings[0];
97         var filename = $(filenamecell).text();
99         ajax.call([{
100             // Get the table data via webservice.
101             methodname: 'core_backup_get_async_backup_links_backup',
102             args: {
103                 'filename': filename,
104                 'contextid': contextid
105             },
106         }])[0].done(function(response) {
107             // We have the data now update the UI.
108             var context = {
109                     filename: filename,
110                     time: timevalue,
111                     size: response.filesize,
112                     fileurl: response.fileurl,
113                     restoreurl: response.restoreurl
114                     };
116             Templates.render('core/async_backup_progress_row', context).then(function(html, js) {
117                 Templates.replaceNodeContents(tablerow, html, js);
118                 return;
119             }).fail(function() {
120                 notification.exception(new Error('Failed to load table row'));
121                 return;
122             });
123         });
124     }
126     /**
127      * Update restore table row when an async restore completes.
128      *
129      * @param {string} backupid The id to match elements on.
130      */
131     function updateRestoreTableRow(backupid) {
132         var statuscell = $('#' + backupid + '_bar').parent().parent();
133         var tablerow = statuscell.parent();
134         var cellsiblings = statuscell.siblings();
135         var coursecell = cellsiblings[0];
136         var timecell = cellsiblings[1];
137         var timevalue = $(timecell).text();
139         ajax.call([{
140             // Get the table data via webservice.
141             methodname: 'core_backup_get_async_backup_links_restore',
142             args: {
143                 'backupid': backupid,
144                 'contextid': contextid
145             },
146         }])[0].done(function(response) {
147          // We have the data now update the UI.
148             var resourcename = $(coursecell).text();
149             var context = {
150                     resourcename: resourcename,
151                     restoreurl: response.restoreurl,
152                     time: timevalue
153                     };
155             Templates.render('core/async_restore_progress_row', context).then(function(html, js) {
156                 Templates.replaceNodeContents(tablerow, html, js);
157                 return;
158             }).fail(function() {
159                 notification.exception(new Error('Failed to load table row'));
160                 return;
161             });
162         });
163     }
165     /**
166      * Update copy table row when an course copy completes.
167      *
168      * @param {string} backupid The id to match elements on.
169      */
170     function updateCopyTableRow(backupid) {
171         var elementbar = document.querySelectorAll("[data-restoreid=" + CSS.escape(backupid) + "]")[0];
172         var restorecourse = elementbar.closest('tr').children[1];
173         var coursename = restorecourse.innerHTML;
174         var courselink = document.createElement('a');
175         var elementbarparent = elementbar.closest('td');
176         var operation = elementbarparent.previousElementSibling;
178         // Replace the prgress bar.
179         Str.get_string('complete').then(function(content) {
180             operation.innerHTML = content;
181             return;
182         }).catch(function() {
183             notification.exception(new Error('Failed to load string: complete'));
184             return;
185         });
187         Templates.render('core/async_copy_complete_cell', {}).then(function(html, js) {
188             Templates.replaceNodeContents(elementbarparent, html, js);
189             return;
190         }).fail(function() {
191             notification.exception(new Error('Failed to load table cell'));
192             return;
193         });
195         // Update the destination course name to a link to that course.
196         ajax.call([{
197             methodname: 'core_backup_get_async_backup_links_restore',
198             args: {
199                 'backupid': backupid,
200                 'contextid': 0
201             },
202         }])[0].done(function(response) {
203             courselink.setAttribute('href', response.restoreurl);
204             courselink.innerHTML = coursename;
205             restorecourse.innerHTML = null;
206             restorecourse.appendChild(courselink);
208             return;
209         }).fail(function() {
210             notification.exception(new Error('Failed to update table row'));
211             return;
212         });
213     }
215     /**
216      * Update the Moodle user interface with the progress of
217      * the backup process.
218      *
219      * @param {object} progress The progress and status of the process.
220      */
221     function updateProgress(progress) {
222         var percentage = progress.progress * 100;
223         var type = 'backup';
224         var elementbar = document.querySelectorAll("[data-" + type + "id=" + CSS.escape(backupid) + "]")[0];
225         var elementstatus = $('#' + backupid + '_status');
226         var elementdetail = $('#' + backupid + '_detail');
227         var elementbutton = $('#' + backupid + '_button');
228         var stringRequests;
230         if (progress.status == STATUS_EXECUTING) {
231             // Process is in progress.
232             // Add in progress class color to bar.
233             elementbar.classList.add('bg-success');
235             updateElement(backupid, type, percentage);
237             // Change heading.
238             var strProcessing = 'async' + typeid + 'processing';
239             Str.get_string(strProcessing, 'backup').then(function(title) {
240                 elementstatus.text(title);
241                 return;
242             }).catch(function() {
243                 notification.exception(new Error('Failed to load string: backup ' + strProcessing));
244             });
246         } else if (progress.status == STATUS_FINISHED_ERR) {
247             // Process completed with error.
249             // Add in fail class color to bar.
250             elementbar.classList.add('bg-danger');
252             // Remove in progress class color to bar.
253             elementbar.classList.remove('bg-success');
255             updateElement(backupid, type, 100);
257             // Change heading and text.
258             var strStatus = 'async' + typeid + 'error';
259             var strStatusDetail = 'async' + typeid + 'errordetail';
260             stringRequests = [
261                 {key: strStatus, component: 'backup'},
262                 {key: strStatusDetail, component: 'backup'}
263             ];
264             Str.get_strings(stringRequests).then(function(strings) {
265                 elementstatus.text(strings[0]);
266                 elementdetail.text(strings[1]);
268                 return;
269             })
270             .catch(function() {
271                 notification.exception(new Error('Failed to load string'));
272                 return;
273             });
275             $('.backup_progress').children('span').removeClass('backup_stage_current');
276             $('.backup_progress').children('span').last().addClass('backup_stage_current');
278             // Stop checking when we either have an error or a completion.
279             clearInterval(backupintervalid);
281         } else if (progress.status == STATUS_FINISHED_OK) {
282             // Process completed successfully.
284             // Add in progress class color to bar
285             elementbar.classList.add('bg-success');
287             updateElement(backupid, type, 100);
289             // Change heading and text
290             var strComplete = 'async' + typeid + 'complete';
291             Str.get_string(strComplete, 'backup').then(function(title) {
292                 elementstatus.text(title);
293                 return;
294             }).catch(function() {
295                 notification.exception(new Error('Failed to load string: backup ' + strComplete));
296             });
298             if (typeid == 'restore') {
299                 ajax.call([{
300                     // Get the table data via webservice.
301                     methodname: 'core_backup_get_async_backup_links_restore',
302                     args: {
303                         'backupid': backupid,
304                         'contextid': contextid
305                     },
306                 }])[0].done(function(response) {
307                     var strDetail = 'async' + typeid + 'completedetail';
308                     var strButton = 'async' + typeid + 'completebutton';
309                     var stringRequests = [
310                         {key: strDetail, component: 'backup', param: response.restoreurl},
311                         {key: strButton, component: 'backup'}
312                     ];
313                     Str.get_strings(stringRequests).then(function(strings) {
314                         elementdetail.html(strings[0]);
315                         elementbutton.text(strings[1]);
316                         elementbutton.attr('href', response.restoreurl);
318                         return;
319                     })
320                     .catch(function() {
321                         notification.exception(new Error('Failed to load string'));
322                         return;
323                     });
325                 });
326             } else {
327                 var strDetail = 'async' + typeid + 'completedetail';
328                 var strButton = 'async' + typeid + 'completebutton';
329                 stringRequests = [
330                     {key: strDetail, component: 'backup', param: restoreurl},
331                     {key: strButton, component: 'backup'}
332                 ];
333                 Str.get_strings(stringRequests).then(function(strings) {
334                     elementdetail.html(strings[0]);
335                     elementbutton.text(strings[1]);
336                     elementbutton.attr('href', restoreurl);
338                     return;
339                 })
340                 .catch(function() {
341                     notification.exception(new Error('Failed to load string'));
342                     return;
343                 });
345             }
347             $('.backup_progress').children('span').removeClass('backup_stage_current');
348             $('.backup_progress').children('span').last().addClass('backup_stage_current');
350             // Stop checking when we either have an error or a completion.
351             clearInterval(backupintervalid);
352         }
353     }
355     /**
356      * Update the Moodle user interface with the progress of
357      * all the pending processes for backup and restore operations.
358      *
359      * @param {object} progress The progress and status of the process.
360      */
361     function updateProgressAll(progress) {
362         progress.forEach(function(element) {
363             var percentage = element.progress * 100;
364             var backupid = element.backupid;
365             var type = element.operation;
366             var elementbar = document.querySelectorAll("[data-" + type + "id=" + CSS.escape(backupid) + "]")[0];
368             if (element.status == STATUS_EXECUTING) {
369                 // Process is in element.
371                 // Add in element class color to bar
372                 elementbar.classList.add('bg-success');
374                 updateElement(backupid, type, percentage);
376             } else if (element.status == STATUS_FINISHED_ERR) {
377                 // Process completed with error.
379                 // Add in fail class color to bar
380                 elementbar.classList.add('bg-danger');
381                 elementbar.classList.add('complete');
383                 // Remove in element class color to bar
384                 elementbar.classList.remove('bg-success');
386                 updateElement(backupid, type, 100);
388             } else if (element.status == STATUS_FINISHED_OK) {
389                 // Process completed successfully.
391                 // Add in element class color to bar
392                 elementbar.classList.add('bg-success');
393                 elementbar.classList.add('complete');
395                 updateElement(backupid, type, 100);
397                 // We have a successful backup. Update the UI with download and file details.
398                 if (type == 'backup') {
399                     updateBackupTableRow(backupid);
400                 } else {
401                     updateRestoreTableRow(backupid);
402                 }
404             }
406         });
407     }
409     /**
410      * Update the Moodle user interface with the progress of
411      * all the pending processes for copy operations.
412      *
413      * @param {object} progress The progress and status of the process.
414      */
415     function updateProgressCopy(progress) {
416         progress.forEach(function(element) {
417             var percentage = element.progress * 100;
418             var backupid = element.backupid;
419             var type = element.operation;
420             var elementbar = document.querySelectorAll("[data-" + type + "id=" + CSS.escape(backupid) + "]")[0];
422             if (type == 'restore') {
423                  let restorecell = elementbar.closest('tr').children[3];
424                  Str.get_string('restore').then(function(content) {
425                      restorecell.innerHTML = content;
426                      return;
427                  }).catch(function() {
428                      notification.exception(new Error('Failed to load string: restore'));
429                  });
430             }
432             if (element.status == STATUS_EXECUTING) {
433                 // Process is in element.
435                 // Add in element class color to bar
436                 elementbar.classList.add('bg-success');
438                 updateElement(backupid, type, percentage);
440             } else if (element.status == STATUS_FINISHED_ERR) {
441                 // Process completed with error.
443                 // Add in fail class color to bar
444                 elementbar.classList.add('bg-danger');
445                 elementbar.classList.add('complete');
447                 // Remove in element class color to bar
448                 elementbar.classList.remove('bg-success');
450                 updateElement(backupid, type, 100);
452             } else if ((element.status == STATUS_FINISHED_OK) && (type == 'restore')) {
453                 // Process completed successfully.
455                 // Add in element class color to bar
456                 elementbar.classList.add('bg-success');
457                 elementbar.classList.add('complete');
459                 updateElement(backupid, type, 100);
461                 // We have a successful copy. Update the UI link to copied course.
462                 updateCopyTableRow(backupid);
463             }
465         });
466     }
468     /**
469      * Get the progress of the backup process via ajax.
470      */
471     function getBackupProgress() {
472         ajax.call([{
473             // Get the backup progress via webservice.
474             methodname: 'core_backup_get_async_backup_progress',
475             args: {
476                 'backupids': [backupid],
477                 'contextid': contextid
478             },
479         }], true, true, false, timeout)[0].done(function(response) {
480             // We have the progress now update the UI.
481             updateProgress(response[0]);
482             checkdelay = checkdelayoriginal;
483             backupintervalid = updateInterval(backupintervalid, getBackupProgress, checkdelayoriginal);
484         }).fail(function() {
485             checkdelay = checkdelay * checkdelaymultipler;
486             backupintervalid = updateInterval(backupintervalid, getBackupProgress, checkdelay);
487         });
488     }
490     /**
491      * Get the progress of all backup processes via ajax.
492      */
493     function getAllBackupProgress() {
494         var backupids = [];
495         var progressbars = $('.progress').find('.progress-bar').not('.complete');
497         progressbars.each(function() {
498             backupids.push((this.id).substring(0, 32));
499         });
501         if (backupids.length > 0) {
502             ajax.call([{
503                 // Get the backup progress via webservice.
504                 methodname: 'core_backup_get_async_backup_progress',
505                 args: {
506                     'backupids': backupids,
507                     'contextid': contextid
508                 },
509             }], true, true, false, timeout)[0].done(function(response) {
510                 updateProgressAll(response);
511                 checkdelay = checkdelayoriginal;
512                 allbackupintervalid = updateInterval(allbackupintervalid, getAllBackupProgress, checkdelayoriginal);
513             }).fail(function() {
514                 checkdelay = checkdelay * checkdelaymultipler;
515                 allbackupintervalid = updateInterval(allbackupintervalid, getAllBackupProgress, checkdelay);
516             });
517         } else {
518             clearInterval(allbackupintervalid); // No more progress bars to update, stop checking.
519         }
520     }
522     /**
523      * Get the progress of all copy processes via ajax.
524      */
525     function getAllCopyProgress() {
526         var copyids = [];
527         var progressbars = $('.progress').find('.progress-bar').not('.complete');
529         progressbars.each(function() {
530             let progressvars = {
531                     'backupid': this.dataset.backupid,
532                     'restoreid': this.dataset.restoreid,
533                     'operation': this.dataset.operation,
534             };
535             copyids.push(progressvars);
536         });
538         if (copyids.length > 0) {
539             ajax.call([{
540                 // Get the copy progress via webservice.
541                 methodname: 'core_backup_get_copy_progress',
542                 args: {
543                     'copies': copyids
544                 },
545             }], true, true, false, timeout)[0].done(function(response) {
546                 updateProgressCopy(response);
547                 checkdelay = checkdelayoriginal;
548                 allcopyintervalid = updateInterval(allcopyintervalid, getAllCopyProgress, checkdelayoriginal);
549             }).fail(function() {
550                 checkdelay = checkdelay * checkdelaymultipler;
551                 allcopyintervalid = updateInterval(allcopyintervalid, getAllCopyProgress, checkdelay);
552             });
553         } else {
554             clearInterval(allcopyintervalid); // No more progress bars to update, stop checking.
555         }
556     }
558     /**
559      * Get status updates for all backups.
560      *
561      * @public
562      * @param {number} context The context id.
563      */
564     Asyncbackup.asyncBackupAllStatus = function(context) {
565         contextid = context;
566         allbackupintervalid = setInterval(getAllBackupProgress, checkdelay);
567     };
569     /**
570      * Get status updates for all course copies.
571      *
572      * @public
573      */
574     Asyncbackup.asyncCopyAllStatus = function() {
575         allcopyintervalid = setInterval(getAllCopyProgress, checkdelay);
576     };
578     /**
579      * Get status updates for backup.
580      *
581      * @public
582      * @param {string} backup The backup record id.
583      * @param {number} context The context id.
584      * @param {string} restore The restore link.
585      * @param {string} type The operation type (backup or restore).
586      */
587     Asyncbackup.asyncBackupStatus = function(backup, context, restore, type) {
588         backupid = backup;
589         contextid = context;
590         restoreurl = restore;
592         if (type == 'backup') {
593             typeid = 'backup';
594         } else {
595             typeid = 'restore';
596         }
598         // Remove the links from the progress bar, no going back now.
599         $('.backup_progress').children('a').removeAttr('href');
601         //  Periodically check for progress updates and update the UI as required.
602         backupintervalid = setInterval(getBackupProgress, checkdelay);
604       };
606       return Asyncbackup;
607 });