Redesign cPanel restore plugin to match WHM workflow
Replace the 8-card category grid with a unified restore workflow: - index.live.cgi: now serves as Step 1 (remote + snapshot selection) - restore.live.cgi: Step 2 (Full Account/Selective mode toggle with type checkboxes, exclude paths, file browser), Step 3 (multi-type confirmation), Step 4 (multi-type execution via AdminBin) Also update cPanel plugin icon from gniza.svg source. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.5 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 685 B |
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/local/cpanel/3rdparty/bin/perl
|
#!/usr/local/cpanel/3rdparty/bin/perl
|
||||||
# gniza cPanel Plugin — Restore Category Grid
|
# gniza cPanel Plugin — Step 1: Select Remote + Snapshot
|
||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings;
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ my $remotes_raw = eval { Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'LIST_
|
|||||||
my @remotes = grep { $_ ne '' } split /\n/, $remotes_raw;
|
my @remotes = grep { $_ ne '' } split /\n/, $remotes_raw;
|
||||||
|
|
||||||
print GnizaCPanel::UI::page_header('GNIZA Backups');
|
print GnizaCPanel::UI::page_header('GNIZA Backups');
|
||||||
|
print GnizaCPanel::UI::render_flash();
|
||||||
|
|
||||||
if (!@remotes) {
|
if (!@remotes) {
|
||||||
print qq{<div class="alert alert-info mb-4">No backup remotes are available for restore. Please contact your server administrator.</div>\n};
|
print qq{<div class="alert alert-info mb-4">No backup remotes are available for restore. Please contact your server administrator.</div>\n};
|
||||||
@@ -36,76 +37,111 @@ if (!@remotes) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Category cards
|
print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
|
||||||
my @categories = (
|
print qq{<h2 class="card-title text-sm">Select Backup Source</h2>\n};
|
||||||
{
|
|
||||||
type => 'account',
|
|
||||||
label => 'Full Backup',
|
|
||||||
desc => 'Restore entire account from backup',
|
|
||||||
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/></svg>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type => 'files',
|
|
||||||
label => 'Home Directory',
|
|
||||||
desc => 'Restore files and folders',
|
|
||||||
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type => 'database',
|
|
||||||
label => 'Databases',
|
|
||||||
desc => 'Restore MySQL databases',
|
|
||||||
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4.03 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5"/></svg>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type => 'dbusers',
|
|
||||||
label => 'Database Users',
|
|
||||||
desc => 'Restore database users and grants',
|
|
||||||
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"/></svg>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type => 'cron',
|
|
||||||
label => 'Cron Jobs',
|
|
||||||
desc => 'Restore scheduled tasks',
|
|
||||||
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type => 'domains',
|
|
||||||
label => 'Domains',
|
|
||||||
desc => 'Restore domain and DNS configuration',
|
|
||||||
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"/></svg>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type => 'ssl',
|
|
||||||
label => 'SSL Certificates',
|
|
||||||
desc => 'Restore SSL certificates',
|
|
||||||
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"/></svg>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type => 'mailbox',
|
|
||||||
label => 'Email Accounts',
|
|
||||||
desc => 'Restore mailboxes and email',
|
|
||||||
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"/></svg>',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
print qq{<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">\n};
|
# Remote dropdown
|
||||||
|
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
|
||||||
|
print qq{ <label class="w-36 font-medium text-sm" for="remote">Remote</label>\n};
|
||||||
|
print qq{ <select class="select select-bordered select-sm w-full max-w-xs" id="remote" name="remote" onchange="gnizaLoadSnapshots()">\n};
|
||||||
|
print qq{ <option value="">-- Select remote --</option>\n};
|
||||||
|
for my $r (@remotes) {
|
||||||
|
my $esc = GnizaCPanel::UI::esc($r);
|
||||||
|
print qq{ <option value="$esc">$esc</option>\n};
|
||||||
|
}
|
||||||
|
print qq{ </select>\n};
|
||||||
|
print qq{</div>\n};
|
||||||
|
|
||||||
for my $cat (@categories) {
|
# Snapshot dropdown (populated via AJAX)
|
||||||
my $type = GnizaCPanel::UI::esc($cat->{type});
|
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
|
||||||
my $label = GnizaCPanel::UI::esc($cat->{label});
|
print qq{ <label class="w-36 font-medium text-sm" for="timestamp">Snapshot</label>\n};
|
||||||
my $desc = GnizaCPanel::UI::esc($cat->{desc});
|
print qq{ <select class="select select-bordered select-sm w-full max-w-xs" id="timestamp" name="timestamp" disabled>\n};
|
||||||
my $icon = $cat->{icon}; # Already safe SVG
|
print qq{ <option value="">-- Select remote first --</option>\n};
|
||||||
|
print qq{ </select>\n};
|
||||||
|
print qq{</div>\n};
|
||||||
|
|
||||||
print qq{<a href="restore.live.cgi?type=$type" class="card bg-white shadow-sm border border-base-300 hover:shadow-md transition-shadow no-underline">\n};
|
print qq{</div>\n</div>\n};
|
||||||
print qq{<div class="card-body items-center text-center">\n};
|
|
||||||
print qq{ <div class="text-primary mb-2">$icon</div>\n};
|
print qq{<div class="flex items-center gap-2">\n};
|
||||||
print qq{ <h2 class="card-title text-sm">$label</h2>\n};
|
print qq{ <button type="button" class="btn btn-primary btn-sm" id="next-btn" disabled onclick="gnizaGoNext()">Next</button>\n};
|
||||||
print qq{ <p class="text-xs text-base-content/60">$desc</p>\n};
|
print qq{</div>\n};
|
||||||
print qq{</div>\n};
|
|
||||||
print qq{</a>\n};
|
# JavaScript for snapshot loading and navigation
|
||||||
|
print <<"END_JS";
|
||||||
|
<script>
|
||||||
|
function gnizaLoadSnapshots() {
|
||||||
|
var remote = document.getElementById('remote').value;
|
||||||
|
var sel = document.getElementById('timestamp');
|
||||||
|
var btn = document.getElementById('next-btn');
|
||||||
|
|
||||||
|
if (!remote) {
|
||||||
|
_setSelectPlaceholder(sel, '-- Select remote first --');
|
||||||
|
sel.disabled = true;
|
||||||
|
btn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setSelectPlaceholder(sel, 'Loading...');
|
||||||
|
sel.disabled = true;
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
var url = 'restore.live.cgi?step=fetch_snapshots&remote=' + encodeURIComponent(remote);
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', url, true);
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState !== 4) return;
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
try {
|
||||||
|
var data = JSON.parse(xhr.responseText);
|
||||||
|
if (data.error) {
|
||||||
|
_setSelectPlaceholder(sel, 'Error: ' + data.error);
|
||||||
|
} else if (data.snapshots && data.snapshots.length > 0) {
|
||||||
|
_populateSelect(sel, data.snapshots);
|
||||||
|
sel.disabled = false;
|
||||||
|
btn.disabled = false;
|
||||||
|
} else {
|
||||||
|
_setSelectPlaceholder(sel, 'No snapshots found');
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
_setSelectPlaceholder(sel, 'Failed to parse response');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_setSelectPlaceholder(sel, 'Request failed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
print qq{</div>\n};
|
function _setSelectPlaceholder(sel, text) {
|
||||||
|
while (sel.options.length) sel.remove(0);
|
||||||
|
var opt = document.createElement('option');
|
||||||
|
opt.value = '';
|
||||||
|
opt.textContent = text;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _populateSelect(sel, values) {
|
||||||
|
while (sel.options.length) sel.remove(0);
|
||||||
|
for (var i = 0; i < values.length; i++) {
|
||||||
|
var opt = document.createElement('option');
|
||||||
|
opt.value = values[i];
|
||||||
|
opt.textContent = values[i];
|
||||||
|
sel.appendChild(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function gnizaGoNext() {
|
||||||
|
var remote = document.getElementById('remote').value;
|
||||||
|
var timestamp = document.getElementById('timestamp').value;
|
||||||
|
if (!remote || !timestamp) return;
|
||||||
|
|
||||||
|
var url = 'restore.live.cgi?step=2'
|
||||||
|
+ '&remote=' + encodeURIComponent(remote)
|
||||||
|
+ '×tamp=' + encodeURIComponent(timestamp);
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
END_JS
|
||||||
|
|
||||||
print GnizaCPanel::UI::page_footer();
|
print GnizaCPanel::UI::page_footer();
|
||||||
print $cpanel->footer();
|
print $cpanel->footer();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user