Compare commits
No commits in common. "2de82f4b2efbfddbabe3d5cf47187af9c2eea9e4" and "a0ca9a5e9fd4b67064101275d8475980ef1f0dca" have entirely different histories.
2de82f4b2e
...
a0ca9a5e9f
@ -1211,117 +1211,3 @@
|
|||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
color: var(--text-secondary, rgba(var(--text-rgb), 0.65));
|
color: var(--text-secondary, rgba(var(--text-rgb), 0.65));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
Per-app Backups tab — Services-style snapshot rows.
|
|
||||||
Each row is a .task-item + .task-header + .task-details so it
|
|
||||||
inherits the global task-list visual (shared with services). The
|
|
||||||
only backup-specific styling is the location pill colour, the
|
|
||||||
ID chip, the deep-link highlight flash, and the inline tags.
|
|
||||||
============================================================ */
|
|
||||||
.backup-snapshot-rows {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 14px;
|
|
||||||
}
|
|
||||||
.backup-snapshot-loc-pill {
|
|
||||||
background: rgba(var(--accent-rgb), 0.15);
|
|
||||||
color: var(--accent);
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 2px 9px;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
.backup-snapshot-id-chip {
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: rgba(var(--text-rgb), 0.55);
|
|
||||||
padding: 1px 6px;
|
|
||||||
background: rgba(var(--text-rgb), 0.05);
|
|
||||||
border-radius: 4px;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
.backup-snapshot-tag {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 4px 2px 0;
|
|
||||||
padding: 1px 6px;
|
|
||||||
background: rgba(var(--text-rgb), 0.06);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: rgba(var(--text-rgb), 0.7);
|
|
||||||
}
|
|
||||||
.backup-snapshot-overflow {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
color: rgba(var(--text-rgb), 0.5);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.backup-snapshot-overflow a { color: var(--accent); }
|
|
||||||
|
|
||||||
/* Deep-link arrival: ?snapshot=<id> flashes the row briefly so the
|
|
||||||
user's eye lands on the right thing after the SPA jump. */
|
|
||||||
.backup-snapshot-flash {
|
|
||||||
animation: backup-snapshot-flash 2.2s ease-out;
|
|
||||||
}
|
|
||||||
@keyframes backup-snapshot-flash {
|
|
||||||
0% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0.55); }
|
|
||||||
20% { box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.55); }
|
|
||||||
100% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0.0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
Global Backup dashboard — app tile + "Back up" action pill.
|
|
||||||
The whole tile navigates to the per-app Backups tab; the pill
|
|
||||||
is the explicit affordance for the old "open the pick modal"
|
|
||||||
behaviour. Hidden by default; visible on hover/focus so the
|
|
||||||
tile stays calm at rest.
|
|
||||||
============================================================ */
|
|
||||||
.backup-app-tile {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.backup-app-tile-action {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--accent);
|
|
||||||
background: rgba(var(--accent-rgb), 0.14);
|
|
||||||
border: 1px solid rgba(var(--accent-rgb), 0.4);
|
|
||||||
border-radius: 999px;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
transition: opacity .15s ease, transform .15s ease, background .15s ease;
|
|
||||||
}
|
|
||||||
.backup-app-tile:hover .backup-app-tile-action,
|
|
||||||
.backup-app-tile:focus-within .backup-app-tile-action {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
.backup-app-tile-action:hover {
|
|
||||||
background: rgba(var(--accent-rgb), 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
Global Snapshots table — App + ID cells link to the per-app
|
|
||||||
page deep-linked to that snapshot.
|
|
||||||
============================================================ */
|
|
||||||
.backup-snapshot-link {
|
|
||||||
color: var(--text-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
border-bottom: 1px dashed rgba(var(--accent-rgb), 0.4);
|
|
||||||
transition: color .15s ease, border-color .15s ease;
|
|
||||||
}
|
|
||||||
.backup-snapshot-link:hover {
|
|
||||||
color: var(--accent);
|
|
||||||
border-bottom-color: var(--accent);
|
|
||||||
}
|
|
||||||
a.backup-snapshot-link.backup-snapshot-id {
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
// Per-app backup view — Services-tab-style list inside the app detail
|
// Per-app backup card — used inside the app detail "Backups" tab.
|
||||||
// "Backups" tab. Each snapshot is a collapsible row (status dot, location
|
// Lightweight view that lists the app's snapshots across all enabled repos and
|
||||||
// pill, "when" chip, ID chip, action buttons + an expandable detail block
|
// offers Backup Now + per-snapshot restore. For full management (delete,
|
||||||
// matching the same .task-item visual pattern the Services tab uses).
|
// migrate, schedule overrides) the user follows the link to /backup.
|
||||||
//
|
|
||||||
// Deep-link contract: /app/<name>/backups?snapshot=<id> auto-expands and
|
|
||||||
// scrolls to that row, so the global Snapshots table on /backup can jump
|
|
||||||
// straight to a specific backup on its app's page.
|
|
||||||
|
|
||||||
class BackupAppCard {
|
class BackupAppCard {
|
||||||
constructor(appName) {
|
constructor(appName) {
|
||||||
@ -32,20 +28,7 @@ class BackupAppCard {
|
|||||||
|
|
||||||
const restoreBtn = e.target.closest('[data-action="restore-app-snapshot"]');
|
const restoreBtn = e.target.closest('[data-action="restore-app-snapshot"]');
|
||||||
if (restoreBtn) {
|
if (restoreBtn) {
|
||||||
e.stopPropagation(); // don't also collapse/expand the row
|
|
||||||
card.restoreSnapshot(restoreBtn.dataset.loc, restoreBtn.dataset.snapshot);
|
card.restoreSnapshot(restoreBtn.dataset.loc, restoreBtn.dataset.snapshot);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header click → toggle the detail panel. Mirrors the
|
|
||||||
// .task-header click target Services uses.
|
|
||||||
const header = e.target.closest('.backup-snapshot-item .task-header');
|
|
||||||
if (header) {
|
|
||||||
const item = header.closest('.backup-snapshot-item');
|
|
||||||
if (item) {
|
|
||||||
const details = item.querySelector('.task-details');
|
|
||||||
if (details) details.classList.toggle('task-details-open');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -75,81 +58,30 @@ class BackupAppCard {
|
|||||||
<span class="backup-card-hint">${allSnaps.length} total across ${locCount} location${locCount === 1 ? '' : 's'}</span>
|
<span class="backup-card-hint">${allSnaps.length} total across ${locCount} location${locCount === 1 ? '' : 's'}</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const iconUrl = `/icons/apps/${encodeURIComponent(this.appName)}.svg`;
|
|
||||||
snapsEl.innerHTML = `
|
snapsEl.innerHTML = `
|
||||||
<div class="backup-snapshot-rows">
|
<table class="backup-snapshot-table" style="margin-top:14px">
|
||||||
${allSnaps.slice(0, 50).map(s => this._renderRow(s, iconUrl)).join('')}
|
<thead>
|
||||||
</div>
|
<tr>
|
||||||
${allSnaps.length > 50 ? `<div class="backup-snapshot-overflow">Showing the most recent 50 of ${allSnaps.length} snapshots. Use the <a href="/backup">backup center</a> for the full list.</div>` : ''}
|
<th>Location</th>
|
||||||
|
<th>When</th>
|
||||||
|
<th>ID</th>
|
||||||
|
<th class="backup-col-actions"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${allSnaps.slice(0, 15).map(s => `
|
||||||
|
<tr>
|
||||||
|
<td>${this.escape(s.locName)}</td>
|
||||||
|
<td>${this.formatRelative(s.time)}</td>
|
||||||
|
<td class="backup-snapshot-id">${this.escape(s.id)}</td>
|
||||||
|
<td class="backup-col-actions">
|
||||||
|
<button class="backup-row-action-btn" data-action="restore-app-snapshot" data-loc="${s.locIdx}" data-snapshot="${this.escape(s.id)}">Restore</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Deep-link: /app/<name>/backups?snapshot=<id> auto-expands that row
|
|
||||||
// and scrolls it into view, briefly flashing the highlight class so
|
|
||||||
// the user's eye lands on the right thing.
|
|
||||||
this._honorSnapshotDeepLink();
|
|
||||||
}
|
|
||||||
|
|
||||||
_renderRow(s, iconUrl) {
|
|
||||||
const sid = String(s.id || '');
|
|
||||||
return `
|
|
||||||
<div class="task-item backup-snapshot-item" data-snapshot="${this.escape(sid)}" data-loc="${this.escape(String(s.locIdx))}">
|
|
||||||
<div class="task-header">
|
|
||||||
<div class="task-info">
|
|
||||||
<img src="${iconUrl}" alt="${this.escape(this.appName)}" class="task-app-icon" onerror="this.style.display='none'">
|
|
||||||
<span class="task-title">${this.escape(this.formatRelative(s.time))}</span>
|
|
||||||
<span class="task-status backup-snapshot-loc-pill">${this.escape(s.locName)}</span>
|
|
||||||
<span class="task-time" title="${this.escape(this._fmtFull(s.time))}">${this.escape(this._fmtShort(s.time))}</span>
|
|
||||||
<span class="backup-snapshot-id-chip" title="Snapshot ID">${this.escape(sid)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="task-actions">
|
|
||||||
<button class="task-btn" data-action="restore-app-snapshot" data-loc="${this.escape(String(s.locIdx))}" data-snapshot="${this.escape(sid)}" title="Restore from this snapshot">
|
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v6h6"/></svg>
|
|
||||||
<span class="task-btn-label">Restore</span>
|
|
||||||
</button>
|
|
||||||
<button class="task-btn toggle-details" data-action="toggle-snapshot" title="Toggle details">
|
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6,9 12,15 18,9"></polyline></svg>
|
|
||||||
<span class="task-btn-label">Details</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="task-details">
|
|
||||||
<div class="task-meta">
|
|
||||||
<div class="meta-item"><strong>Snapshot ID:</strong> <code>${this.escape(sid)}</code></div>
|
|
||||||
<div class="meta-item"><strong>Location:</strong> ${this.escape(s.locName)}</div>
|
|
||||||
<div class="meta-item"><strong>When:</strong> ${this.escape(this._fmtFull(s.time))}</div>
|
|
||||||
${s.hostname ? `<div class="meta-item"><strong>Host:</strong> ${this.escape(s.hostname)}</div>` : ''}
|
|
||||||
${s.tags && s.tags.length ? `<div class="meta-item"><strong>Tags:</strong> ${s.tags.map(t => `<span class="backup-snapshot-tag">${this.escape(t)}</span>`).join(' ')}</div>` : ''}
|
|
||||||
${s.paths && s.paths.length ? `<div class="meta-item"><strong>Paths:</strong><br><code>${s.paths.map(p => this.escape(p)).join('<br>')}</code></div>` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
_honorSnapshotDeepLink() {
|
|
||||||
const want = new URLSearchParams(window.location.search).get('snapshot');
|
|
||||||
if (!want) return;
|
|
||||||
const row = document.querySelector(`.backup-snapshot-item[data-snapshot="${CSS.escape(want)}"]`);
|
|
||||||
if (!row) return;
|
|
||||||
const details = row.querySelector('.task-details');
|
|
||||||
if (details && !details.classList.contains('task-details-open')) details.classList.add('task-details-open');
|
|
||||||
setTimeout(() => {
|
|
||||||
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
row.classList.add('backup-snapshot-flash');
|
|
||||||
setTimeout(() => row.classList.remove('backup-snapshot-flash'), 2200);
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
_fmtShort(iso) {
|
|
||||||
if (!iso) return '';
|
|
||||||
const d = new Date(iso);
|
|
||||||
if (isNaN(d.getTime())) return iso;
|
|
||||||
return d.toLocaleString();
|
|
||||||
}
|
|
||||||
_fmtFull(iso) {
|
|
||||||
if (!iso) return '';
|
|
||||||
const d = new Date(iso);
|
|
||||||
if (isNaN(d.getTime())) return iso;
|
|
||||||
return d.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadData() {
|
async loadData() {
|
||||||
@ -186,10 +118,7 @@ class BackupAppCard {
|
|||||||
locIdx,
|
locIdx,
|
||||||
locName: this.locationsByIdx[locIdx]?.name || `Location ${locIdx}`,
|
locName: this.locationsByIdx[locIdx]?.name || `Location ${locIdx}`,
|
||||||
time: s.time,
|
time: s.time,
|
||||||
id: s.short_id || (s.id || '').slice(0, 8),
|
id: s.short_id || (s.id || '').slice(0, 8)
|
||||||
hostname: s.hostname || '',
|
|
||||||
tags,
|
|
||||||
paths: Array.isArray(s.paths) ? s.paths : [],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -186,43 +186,15 @@ class BackupPage {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deep-link from the Snapshots table → /app/<name>/backups?snapshot=<id>.
|
// Click any tile on the dashboard → open the Back-up checklist
|
||||||
// Routed via the SPA so the app page mounts in-place rather than a
|
// modal with that tile pre-ticked. System tile is data-system="1";
|
||||||
// full reload.
|
// app tiles carry data-app="<slug>". Both share .backup-app-tile.
|
||||||
const deepLink = e.target.closest('[data-deep-link]');
|
|
||||||
if (deepLink) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (window.navigateToRoute) window.navigateToRoute(deepLink.dataset.deepLink);
|
|
||||||
else window.location.href = deepLink.dataset.deepLink;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tile actions, in priority order:
|
|
||||||
// 1. "Backup now" pill on the tile → opens the pick modal
|
|
||||||
// preticked with that tile (explicit affordance, replaces
|
|
||||||
// the old implicit whole-tile click).
|
|
||||||
// 2. Whole-tile click → navigates to the per-app Backups tab
|
|
||||||
// (or the system page for the System tile). This is the
|
|
||||||
// cohesion fix: each tile leads to the page that owns
|
|
||||||
// that subject's full detail, not a modal asking "do
|
|
||||||
// you want to back up?".
|
|
||||||
const backupNowBtn = e.target.closest('[data-action="backup-now"]');
|
|
||||||
if (backupNowBtn) {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (backupNowBtn.dataset.system) {
|
|
||||||
this.openBackupPickModal({ preTickSystem: true });
|
|
||||||
} else if (backupNowBtn.dataset.app) {
|
|
||||||
this.openBackupPickModal({ preTickApps: [backupNowBtn.dataset.app] });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tile = e.target.closest('.backup-app-tile');
|
const tile = e.target.closest('.backup-app-tile');
|
||||||
if (tile) {
|
if (tile) {
|
||||||
if (tile.dataset.system) {
|
if (tile.dataset.system) {
|
||||||
// System has no dedicated page yet — keep the pick modal.
|
|
||||||
this.openBackupPickModal({ preTickSystem: true });
|
this.openBackupPickModal({ preTickSystem: true });
|
||||||
} else if (tile.dataset.app && window.navigateToRoute) {
|
} else if (tile.dataset.app) {
|
||||||
window.navigateToRoute(`/app/${encodeURIComponent(tile.dataset.app)}/backups`);
|
this.openBackupPickModal({ preTickApps: [tile.dataset.app] });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -671,7 +643,7 @@ class BackupPage {
|
|||||||
const dot = has ? 'ok' : 'none';
|
const dot = has ? 'ok' : 'none';
|
||||||
const when = has ? 'Last backed up ' + this.formatRelative(sys.latest_time) : 'No backup yet';
|
const when = has ? 'Last backed up ' + this.formatRelative(sys.latest_time) : 'No backup yet';
|
||||||
return `
|
return `
|
||||||
<div class="backup-app-tile backup-app-tile--system" data-system="1" title="Back up system config">
|
<div class="backup-app-tile backup-app-tile--system" data-system="1">
|
||||||
<img class="backup-app-tile-icon" src="/icons/apps/libreportal.svg" alt="" onerror="this.style.display='none'">
|
<img class="backup-app-tile-icon" src="/icons/apps/libreportal.svg" alt="" onerror="this.style.display='none'">
|
||||||
<div class="backup-app-tile-text">
|
<div class="backup-app-tile-text">
|
||||||
<div class="backup-app-tile-name">System config</div>
|
<div class="backup-app-tile-name">System config</div>
|
||||||
@ -680,9 +652,6 @@ class BackupPage {
|
|||||||
<span>${this.escape(when)}</span>
|
<span>${this.escape(when)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="backup-app-tile-action" data-action="backup-now" data-system="1" title="Back up now">
|
|
||||||
Back up
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -720,7 +689,7 @@ class BackupPage {
|
|||||||
const when = has ? this.formatRelative(app.latest_time) : 'No backup yet';
|
const when = has ? this.formatRelative(app.latest_time) : 'No backup yet';
|
||||||
const { icon, displayName } = this.appMeta(app.app);
|
const { icon, displayName } = this.appMeta(app.app);
|
||||||
return `
|
return `
|
||||||
<div class="backup-app-tile" data-app="${this.escape(app.app)}" title="Open ${this.escape(displayName)} backup history">
|
<div class="backup-app-tile" data-app="${this.escape(app.app)}">
|
||||||
<img class="backup-app-tile-icon" src="${this.escape(icon)}" alt="" onerror="this.src='/icons/apps/default.svg'">
|
<img class="backup-app-tile-icon" src="${this.escape(icon)}" alt="" onerror="this.src='/icons/apps/default.svg'">
|
||||||
<div class="backup-app-tile-text">
|
<div class="backup-app-tile-text">
|
||||||
<div class="backup-app-tile-name">${this.escape(displayName)}</div>
|
<div class="backup-app-tile-name">${this.escape(displayName)}</div>
|
||||||
@ -729,9 +698,6 @@ class BackupPage {
|
|||||||
<span>${when}</span>
|
<span>${when}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="backup-app-tile-action" data-action="backup-now" data-app="${this.escape(app.app)}" title="Back up now">
|
|
||||||
Back up
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -1065,35 +1031,19 @@ class BackupPage {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = filtered.map(r => {
|
tbody.innerHTML = filtered.map(r => `
|
||||||
// Link the App and ID cells to the per-app Backups tab with the
|
<tr>
|
||||||
// snapshot pre-expanded. "—" rows (snapshots without an
|
<td>${this.escape(r.app)}</td>
|
||||||
// app=<slug> tag, e.g. system config backups) stay plain text
|
<td>${this.escape(r.host)}</td>
|
||||||
// since there's no app page to open.
|
<td>${this.escape(r.locName)}</td>
|
||||||
const hasApp = r.app && r.app !== '—';
|
<td>${this.formatRelative(r.time)}</td>
|
||||||
const deepLink = hasApp
|
<td class="backup-snapshot-id">${this.escape(r.id)}</td>
|
||||||
? `/app/${encodeURIComponent(r.app)}/backups?snapshot=${encodeURIComponent(r.id)}`
|
<td class="backup-col-actions">
|
||||||
: null;
|
<button class="backup-row-action-btn" data-action="restore-snapshot" data-app="${this.escape(r.app)}" data-loc="${r.locIdx}" data-snapshot="${this.escape(r.id)}">Restore</button>
|
||||||
const appCell = hasApp
|
<button class="backup-row-action-btn danger" data-action="delete-snapshot" data-app="${this.escape(r.app)}" data-loc="${r.locIdx}" data-snapshot="${this.escape(r.id)}">Delete</button>
|
||||||
? `<a class="backup-snapshot-link" href="${this.escape(deepLink)}" data-deep-link="${this.escape(deepLink)}">${this.escape(r.app)}</a>`
|
</td>
|
||||||
: this.escape(r.app);
|
</tr>
|
||||||
const idCell = hasApp
|
`).join('');
|
||||||
? `<a class="backup-snapshot-link backup-snapshot-id" href="${this.escape(deepLink)}" data-deep-link="${this.escape(deepLink)}">${this.escape(r.id)}</a>`
|
|
||||||
: `<span class="backup-snapshot-id">${this.escape(r.id)}</span>`;
|
|
||||||
return `
|
|
||||||
<tr>
|
|
||||||
<td>${appCell}</td>
|
|
||||||
<td>${this.escape(r.host)}</td>
|
|
||||||
<td>${this.escape(r.locName)}</td>
|
|
||||||
<td>${this.formatRelative(r.time)}</td>
|
|
||||||
<td>${idCell}</td>
|
|
||||||
<td class="backup-col-actions">
|
|
||||||
<button class="backup-row-action-btn" data-action="restore-snapshot" data-app="${this.escape(r.app)}" data-loc="${r.locIdx}" data-snapshot="${this.escape(r.id)}">Restore</button>
|
|
||||||
<button class="backup-row-action-btn danger" data-action="delete-snapshot" data-app="${this.escape(r.app)}" data-loc="${r.locIdx}" data-snapshot="${this.escape(r.id)}">Delete</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderConfiguration() {
|
renderConfiguration() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user