feat: snapshot management UI — list and delete database snapshots

- ListBackups API now returns snapshots alongside backups
- DeleteBackup API accepts snapshot- prefixed names
- New "Database Snapshots" panel in admin UI with delete buttons
- i18n: English + Hebrew translations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 20:38:33 +00:00
parent d5fe6c21a0
commit ec759b89c1
6 changed files with 120 additions and 2 deletions
@@ -116,6 +116,19 @@
</div>
</div>
{{! === Snapshots === }}
<div class="panel panel-info">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-camera"></i>
{{translate 'Database Snapshots' scope='DataMigration'}}
</h4>
</div>
<div class="panel-body" style="padding: 0;">
<div class="snapshots-list list-group" style="margin-bottom: 0;"></div>
</div>
</div>
{{! === Import === }}
<div class="import-section" style="display: none;">
<div class="panel panel-warning">
@@ -72,6 +72,11 @@ define('modules/data-migration/views/admin/data-migration', ['view'], function (
const name = $(e.currentTarget).data('name');
this.$el.find('.backup-details[data-name="' + name + '"]').toggle();
},
'click [data-action="deleteSnapshot"]': function (e) {
e.stopPropagation();
const name = $(e.currentTarget).data('name');
this.actionDeleteSnapshot(name);
},
'click [data-action="dismissExportLog"]': function () {
this.exportLog = null;
this.$el.find('.export-log-container').hide();
@@ -144,8 +149,10 @@ define('modules/data-migration/views/admin/data-migration', ['view'], function (
Espo.Ajax.getRequest('DataMigration/action/listBackups')
.then(response => {
this.backups = response.backups || [];
this.snapshots = response.snapshots || [];
this.isLoadingBackups = false;
this.renderBackups();
this.renderSnapshots();
})
.catch(() => {
this.isLoadingBackups = false;
@@ -406,6 +413,60 @@ define('modules/data-migration/views/admin/data-migration', ['view'], function (
);
},
renderSnapshots: function () {
const $container = this.$el.find('.snapshots-list');
if (!this.snapshots || this.snapshots.length === 0) {
$container.html(
'<div class="text-muted" style="padding: 15px; text-align: center;">' +
this.translate('No snapshots', 'labels', 'DataMigration') +
'</div>'
);
return;
}
let html = '';
this.snapshots.forEach(snap => {
const date = snap.createdAt ? new Date(snap.createdAt).toLocaleString() : 'Unknown';
html += '<div class="list-group-item">';
html += '<div class="pull-right">';
html += '<button class="btn btn-danger btn-xs" data-action="deleteSnapshot" data-name="' + snap.name + '" title="' + this.translate('Delete') + '">';
html += '<i class="fas fa-trash"></i></button>';
html += '</div>';
html += '<h5 class="list-group-item-heading" style="margin: 0 0 4px;">';
html += '<i class="fas fa-camera text-info"></i> ' + snap.name;
html += '</h5>';
html += '<p class="list-group-item-text text-muted" style="margin: 0; font-size: 0.9em;">';
html += '<i class="fas fa-clock"></i> ' + date;
html += ' &nbsp;|&nbsp; <i class="fas fa-database"></i> ' + snap.database;
html += ' &nbsp;|&nbsp; <i class="fas fa-hdd"></i> ' + snap.sizeFormatted;
html += '</p>';
html += '</div>';
});
$container.html(html);
},
actionDeleteSnapshot: function (name) {
this.confirm(
this.translate('deleteSnapshotConfirmation', 'messages', 'DataMigration'),
() => {
Espo.Ajax.postRequest('DataMigration/action/deleteBackup', {
backupName: name,
})
.then(() => {
Espo.Ui.success(this.translate('Deleted'));
this.loadBackups();
})
.catch(() => {
Espo.Ui.error(this.translate('Failed to delete snapshot', 'labels', 'DataMigration'));
});
}
);
},
renderLog: function (selector, lines, isSuccess) {
const $container = this.$el.find(selector);