feat(backup): tidy location editor — section dividers, style tooltip, row enable toggle
Make the expanded location editor read like /config: Connection and Retention now use the section header + .domains-divider layout, and Connection gets a description. Move the retention 'Backup style' guidance into a tooltip and drop the always-visible hint line below it. Move the Enabled toggle out of the Connection fields into the collapsed location row header so a location can be enabled/disabled without expanding it; setLocationEnabled persists the change via the same config_update routing as saveSection. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
f3ac9f8684
commit
3b13a67ca7
@ -794,6 +794,12 @@
|
|||||||
transform: translateX(16px);
|
transform: translateX(16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Enable/disable toggle in a location row header — sits between the row
|
||||||
|
info and the expand chevron, controlling the location without expanding it. */
|
||||||
|
.backup-loc-enable-toggle {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Repo card extras */
|
/* Repo card extras */
|
||||||
.backup-repo-stats {
|
.backup-repo-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -52,14 +52,14 @@ const BACKUP_LOC_FIELD_DEFS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BACKUP_LOC_FIELDS_BY_TYPE = {
|
const BACKUP_LOC_FIELDS_BY_TYPE = {
|
||||||
local: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'PATH_MODE', 'PATH', 'APPEND_ONLY'],
|
local: ['NAME', 'ENGINE', 'TYPE', 'PATH_MODE', 'PATH', 'APPEND_ONLY'],
|
||||||
sftp: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'SSH_USER', 'SSH_HOST', 'SSH_PORT', 'SSH_PATH', 'SSH_AUTH', 'SSH_PASS', 'URI', 'APPEND_ONLY'],
|
sftp: ['NAME', 'ENGINE', 'TYPE', 'SSH_USER', 'SSH_HOST', 'SSH_PORT', 'SSH_PATH', 'SSH_AUTH', 'SSH_PASS', 'URI', 'APPEND_ONLY'],
|
||||||
rest: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY'],
|
rest: ['NAME', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY'],
|
||||||
s3: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'URI', 'S3_ACCESS_KEY', 'S3_SECRET_KEY', 'APPEND_ONLY'],
|
s3: ['NAME', 'ENGINE', 'TYPE', 'URI', 'S3_ACCESS_KEY', 'S3_SECRET_KEY', 'APPEND_ONLY'],
|
||||||
b2: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'URI', 'B2_ACCOUNT_ID', 'B2_ACCOUNT_KEY', 'APPEND_ONLY'],
|
b2: ['NAME', 'ENGINE', 'TYPE', 'URI', 'B2_ACCOUNT_ID', 'B2_ACCOUNT_KEY', 'APPEND_ONLY'],
|
||||||
gs: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY'],
|
gs: ['NAME', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY'],
|
||||||
azure: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY'],
|
azure: ['NAME', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY'],
|
||||||
rclone: ['NAME', 'ENABLED', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY']
|
rclone: ['NAME', 'ENGINE', 'TYPE', 'URI', 'APPEND_ONLY']
|
||||||
};
|
};
|
||||||
|
|
||||||
function backupRetentionDetectPreset(values, includeInherit = false) {
|
function backupRetentionDetectPreset(values, includeInherit = false) {
|
||||||
@ -164,6 +164,13 @@ class BackupPage {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const locEnable = e.target.closest('[data-action="toggle-location-enabled"]');
|
||||||
|
if (locEnable) {
|
||||||
|
const cb = locEnable.querySelector('input[type="checkbox"]');
|
||||||
|
this.setLocationEnabled(parseInt(locEnable.dataset.loc, 10), cb ? cb.checked : true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const locHeader = e.target.closest('[data-action="toggle-location"]');
|
const locHeader = e.target.closest('[data-action="toggle-location"]');
|
||||||
if (locHeader) {
|
if (locHeader) {
|
||||||
this.toggleLocationExpand(parseInt(locHeader.dataset.loc, 10));
|
this.toggleLocationExpand(parseInt(locHeader.dataset.loc, 10));
|
||||||
@ -546,6 +553,10 @@ class BackupPage {
|
|||||||
<span class="backup-location-row-sep">·</span>
|
<span class="backup-location-row-sep">·</span>
|
||||||
<span class="backup-location-row-stat">${size}</span>
|
<span class="backup-location-row-stat">${size}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="backup-toggle backup-loc-enable-toggle" data-action="toggle-location-enabled" data-loc="${l.idx}" title="${l.enabled ? 'Enabled — click to disable this location' : 'Disabled — click to enable this location'}">
|
||||||
|
<input type="checkbox" ${l.enabled ? 'checked' : ''} aria-label="Enable this location">
|
||||||
|
<span class="backup-toggle-slider"></span>
|
||||||
|
</span>
|
||||||
<svg class="backup-location-chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="backup-location-chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
@ -585,16 +596,31 @@ class BackupPage {
|
|||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="config-category backup-location-config" data-section="location-${idx}">
|
<div class="config-category backup-location-config" data-section="location-${idx}">
|
||||||
<h3>Connection</h3>
|
<div class="domains-wrapper">
|
||||||
<div class="backup-location-connection-fields" id="backup-location-${idx}-connection">
|
<div class="domains-header">
|
||||||
${this.renderLocFields(idx, connectionFields, l)}
|
<div>
|
||||||
|
<h3>Connection</h3>
|
||||||
|
<p class="category-description">How LibrePortal connects to this storage location.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="domains-divider"></div>
|
||||||
|
<div class="backup-location-connection-fields" id="backup-location-${idx}-connection">
|
||||||
|
${this.renderLocFields(idx, connectionFields, l)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-category backup-location-config">
|
<div class="config-category backup-location-config">
|
||||||
<h3>Retention</h3>
|
<div class="domains-wrapper">
|
||||||
<p class="category-description">When to delete old backups from this location.</p>
|
<div class="domains-header">
|
||||||
<div id="backup-location-${idx}-retention">
|
<div>
|
||||||
${this.formRetention(`CFG_BACKUP_LOC_${idx}_`, retentionValues, true)}
|
<h3>Retention</h3>
|
||||||
|
<p class="category-description">When to delete old backups from this location.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="domains-divider"></div>
|
||||||
|
<div id="backup-location-${idx}-retention">
|
||||||
|
${this.formRetention(`CFG_BACKUP_LOC_${idx}_`, retentionValues, true)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="backup-location-actions">
|
<div class="backup-location-actions">
|
||||||
@ -964,7 +990,6 @@ class BackupPage {
|
|||||||
"Custom…" is selected. */
|
"Custom…" is selected. */
|
||||||
formRetention(prefix, values, includeInherit = false) {
|
formRetention(prefix, values, includeInherit = false) {
|
||||||
const preset = backupRetentionDetectPreset(values, includeInherit);
|
const preset = backupRetentionDetectPreset(values, includeInherit);
|
||||||
const meta = BACKUP_RETENTION_PRESET_META[preset];
|
|
||||||
const presetOptions = Object.entries(BACKUP_RETENTION_PRESET_META)
|
const presetOptions = Object.entries(BACKUP_RETENTION_PRESET_META)
|
||||||
.filter(([k]) => k !== 'inherit-global' || includeInherit)
|
.filter(([k]) => k !== 'inherit-global' || includeInherit)
|
||||||
.map(([k, v]) => `<option value="${k}" ${k === preset ? 'selected' : ''}>${this.escape(v.label)}</option>`)
|
.map(([k, v]) => `<option value="${k}" ${k === preset ? 'selected' : ''}>${this.escape(v.label)}</option>`)
|
||||||
@ -977,10 +1002,9 @@ class BackupPage {
|
|||||||
return `
|
return `
|
||||||
<div class="backup-form-grid backup-retention-block" data-retention-prefix="${this.escape(prefix)}" data-retention-allow-inherit="${includeInherit ? '1' : '0'}">
|
<div class="backup-form-grid backup-retention-block" data-retention-prefix="${this.escape(prefix)}" data-retention-allow-inherit="${includeInherit ? '1' : '0'}">
|
||||||
<label class="backup-form-row">
|
<label class="backup-form-row">
|
||||||
<span class="backup-form-label">Backup style</span>
|
<span class="backup-form-label">Backup style <span class="tooltip" title="Use whatever the Configuration tab specifies. Pick something else here only when this location needs a different policy.">ℹ️</span></span>
|
||||||
<select class="form-control" data-retention-preset>${presetOptions}</select>
|
<select class="form-control" data-retention-preset>${presetOptions}</select>
|
||||||
</label>
|
</label>
|
||||||
<div class="backup-retention-hint backup-card-hint" data-retention-hint>${this.escape(meta?.hint || '')}</div>
|
|
||||||
${customRetentionHidden}
|
${customRetentionHidden}
|
||||||
</div>
|
</div>
|
||||||
<div class="backup-retention-advanced" data-retention-advanced ${preset === 'custom' ? '' : 'hidden'}>
|
<div class="backup-retention-advanced" data-retention-advanced ${preset === 'custom' ? '' : 'hidden'}>
|
||||||
@ -1002,8 +1026,6 @@ class BackupPage {
|
|||||||
const prefix = block.dataset.retentionPrefix;
|
const prefix = block.dataset.retentionPrefix;
|
||||||
const allowInherit = block.dataset.retentionAllowInherit === '1';
|
const allowInherit = block.dataset.retentionAllowInherit === '1';
|
||||||
const preset = selectEl.value;
|
const preset = selectEl.value;
|
||||||
const hintEl = block.querySelector('[data-retention-hint]');
|
|
||||||
if (hintEl) hintEl.textContent = BACKUP_RETENTION_PRESET_META[preset]?.hint || '';
|
|
||||||
|
|
||||||
if (preset === 'custom') {
|
if (preset === 'custom') {
|
||||||
if (advanced) advanced.hidden = false;
|
if (advanced) advanced.hidden = false;
|
||||||
@ -1437,6 +1459,20 @@ class BackupPage {
|
|||||||
|
|
||||||
/* ----- Generic save handler ----- */
|
/* ----- Generic save handler ----- */
|
||||||
|
|
||||||
|
async setLocationEnabled(idx, enabled) {
|
||||||
|
const encoded = `CFG_BACKUP_LOC_${idx}_ENABLED=${enabled ? 'true' : 'false'}`;
|
||||||
|
try {
|
||||||
|
if (!window.tasksManager?.router) throw new Error('Task system not available');
|
||||||
|
await window.tasksManager.router.routeAction('config_update', {
|
||||||
|
changes: `'${encoded.replace(/'/g, "'\\''")}'`
|
||||||
|
});
|
||||||
|
this.notify(`${enabled ? 'Enabling' : 'Disabling'} this location…`, 'success');
|
||||||
|
setTimeout(() => this.reloadAfterSave(), 2500);
|
||||||
|
} catch (err) {
|
||||||
|
this.notify(`Save failed: ${err.message || err}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async saveSection(sectionId) {
|
async saveSection(sectionId) {
|
||||||
let scope;
|
let scope;
|
||||||
if (sectionId.startsWith('location-')) {
|
if (sectionId.startsWith('location-')) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user