Merge claude/2
This commit is contained in:
commit
2de82f4b2e
@ -1211,3 +1211,117 @@
|
|||||||
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,7 +1,11 @@
|
|||||||
// Per-app backup card — used inside the app detail "Backups" tab.
|
// Per-app backup view — Services-tab-style list inside the app detail
|
||||||
// Lightweight view that lists the app's snapshots across all enabled repos and
|
// "Backups" tab. Each snapshot is a collapsible row (status dot, location
|
||||||
// offers Backup Now + per-snapshot restore. For full management (delete,
|
// pill, "when" chip, ID chip, action buttons + an expandable detail block
|
||||||
// migrate, schedule overrides) the user follows the link to /backup.
|
// matching the same .task-item visual pattern the Services tab uses).
|
||||||
|
//
|
||||||
|
// 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) {
|
||||||
@ -28,7 +32,20 @@ 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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -58,30 +75,81 @@ 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 = `
|
||||||
<table class="backup-snapshot-table" style="margin-top:14px">
|
<div class="backup-snapshot-rows">
|
||||||
<thead>
|
${allSnaps.slice(0, 50).map(s => this._renderRow(s, iconUrl)).join('')}
|
||||||
<tr>
|
</div>
|
||||||
<th>Location</th>
|
${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>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() {
|
||||||
@ -118,7 +186,10 @@ 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,15 +186,43 @@ class BackupPage {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click any tile on the dashboard → open the Back-up checklist
|
// Deep-link from the Snapshots table → /app/<name>/backups?snapshot=<id>.
|
||||||
// modal with that tile pre-ticked. System tile is data-system="1";
|
// Routed via the SPA so the app page mounts in-place rather than a
|
||||||
// app tiles carry data-app="<slug>". Both share .backup-app-tile.
|
// full reload.
|
||||||
|
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) {
|
} else if (tile.dataset.app && window.navigateToRoute) {
|
||||||
this.openBackupPickModal({ preTickApps: [tile.dataset.app] });
|
window.navigateToRoute(`/app/${encodeURIComponent(tile.dataset.app)}/backups`);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -643,7 +671,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">
|
<div class="backup-app-tile backup-app-tile--system" data-system="1" title="Back up system config">
|
||||||
<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>
|
||||||
@ -652,6 +680,9 @@ 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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -689,7 +720,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)}">
|
<div class="backup-app-tile" data-app="${this.escape(app.app)}" title="Open ${this.escape(displayName)} backup history">
|
||||||
<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>
|
||||||
@ -698,6 +729,9 @@ 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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -1031,19 +1065,35 @@ class BackupPage {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = filtered.map(r => `
|
tbody.innerHTML = filtered.map(r => {
|
||||||
<tr>
|
// Link the App and ID cells to the per-app Backups tab with the
|
||||||
<td>${this.escape(r.app)}</td>
|
// snapshot pre-expanded. "—" rows (snapshots without an
|
||||||
<td>${this.escape(r.host)}</td>
|
// app=<slug> tag, e.g. system config backups) stay plain text
|
||||||
<td>${this.escape(r.locName)}</td>
|
// since there's no app page to open.
|
||||||
<td>${this.formatRelative(r.time)}</td>
|
const hasApp = r.app && r.app !== '—';
|
||||||
<td class="backup-snapshot-id">${this.escape(r.id)}</td>
|
const deepLink = hasApp
|
||||||
<td class="backup-col-actions">
|
? `/app/${encodeURIComponent(r.app)}/backups?snapshot=${encodeURIComponent(r.id)}`
|
||||||
<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>
|
: null;
|
||||||
<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>
|
const appCell = hasApp
|
||||||
</td>
|
? `<a class="backup-snapshot-link" href="${this.escape(deepLink)}" data-deep-link="${this.escape(deepLink)}">${this.escape(r.app)}</a>`
|
||||||
</tr>
|
: this.escape(r.app);
|
||||||
`).join('');
|
const idCell = hasApp
|
||||||
|
? `<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