adiciona modal para criação e edição de profiles; atualiza estilos e lógica de exibição
This commit is contained in:
parent
ef9ffdbe71
commit
11c58f0cfe
|
|
@ -14,8 +14,9 @@ const elements = {
|
||||||
backupDir: document.querySelector('#backupDir'),
|
backupDir: document.querySelector('#backupDir'),
|
||||||
containerOptions: document.querySelector('#containerOptions'),
|
containerOptions: document.querySelector('#containerOptions'),
|
||||||
profilesList: document.querySelector('#profilesList'),
|
profilesList: document.querySelector('#profilesList'),
|
||||||
formModeBadge: document.querySelector('#formModeBadge'),
|
|
||||||
toast: document.querySelector('#toast'),
|
toast: document.querySelector('#toast'),
|
||||||
|
profileFormModal: document.querySelector('#profileFormModal'),
|
||||||
|
profileModalClose: document.querySelector('#profileModalClose'),
|
||||||
restoreModal: document.querySelector('#restoreModal'),
|
restoreModal: document.querySelector('#restoreModal'),
|
||||||
restoreModalSubtitle: document.querySelector('#restoreModalSubtitle'),
|
restoreModalSubtitle: document.querySelector('#restoreModalSubtitle'),
|
||||||
restoreContainerOptions: document.querySelector('#restoreContainerOptions'),
|
restoreContainerOptions: document.querySelector('#restoreContainerOptions'),
|
||||||
|
|
@ -67,6 +68,31 @@ document.querySelector('.sidebar').addEventListener('click', (e) => {
|
||||||
document.querySelector('#createProfileBtn')?.addEventListener('click', () => navigateTo('profiles'));
|
document.querySelector('#createProfileBtn')?.addEventListener('click', () => navigateTo('profiles'));
|
||||||
document.querySelector('#refreshServers')?.addEventListener('click', () => renderServers());
|
document.querySelector('#refreshServers')?.addEventListener('click', () => renderServers());
|
||||||
|
|
||||||
|
// ─── Profile form modal ───────────────────────────────────
|
||||||
|
function openProfileModal(title = 'Novo Profile') {
|
||||||
|
document.querySelector('#profileModalTitle').textContent = title;
|
||||||
|
elements.profileFormModal.classList.remove('hidden');
|
||||||
|
elements.profileFormModal.setAttribute('aria-hidden', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeProfileModal() {
|
||||||
|
elements.profileFormModal.classList.add('hidden');
|
||||||
|
elements.profileFormModal.setAttribute('aria-hidden', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('#openCreateProfileModal')?.addEventListener('click', () => {
|
||||||
|
resetForm();
|
||||||
|
openProfileModal('Novo Profile');
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.profileModalClose?.addEventListener('click', closeProfileModal);
|
||||||
|
|
||||||
|
elements.profileFormModal?.addEventListener('click', (e) => {
|
||||||
|
if (e.target.closest('[data-action="close-profile-modal"]')) {
|
||||||
|
closeProfileModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function renderServers() {
|
function renderServers() {
|
||||||
const list = document.querySelector('#serversList');
|
const list = document.querySelector('#serversList');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
|
@ -754,9 +780,9 @@ async function renderProfiles() {
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
elements.profileForm.reset();
|
elements.profileForm.reset();
|
||||||
elements.profileId.value = '';
|
elements.profileId.value = '';
|
||||||
elements.formModeBadge.textContent = 'criar';
|
|
||||||
state.volumeSelections = {};
|
state.volumeSelections = {};
|
||||||
renderContainers();
|
renderContainers();
|
||||||
|
closeProfileModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillForm(profile) {
|
function fillForm(profile) {
|
||||||
|
|
@ -765,7 +791,6 @@ function fillForm(profile) {
|
||||||
elements.backupDir.value = profile.backupDir;
|
elements.backupDir.value = profile.backupDir;
|
||||||
const backupScope = profile.backupScope === 'container' ? 'container' : 'volumes';
|
const backupScope = profile.backupScope === 'container' ? 'container' : 'volumes';
|
||||||
document.querySelector(`input[name="backupScope"][value="${backupScope}"]`).checked = true;
|
document.querySelector(`input[name="backupScope"][value="${backupScope}"]`).checked = true;
|
||||||
elements.formModeBadge.textContent = 'editar';
|
|
||||||
state.volumeSelections = Object.assign({}, profile.volumeSelections || {});
|
state.volumeSelections = Object.assign({}, profile.volumeSelections || {});
|
||||||
renderContainers();
|
renderContainers();
|
||||||
for (const containerId of profile.containerIds) {
|
for (const containerId of profile.containerIds) {
|
||||||
|
|
@ -774,7 +799,7 @@ function fillForm(profile) {
|
||||||
input.checked = true;
|
input.checked = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
openProfileModal('Editar Profile');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadContainers() {
|
async function loadContainers() {
|
||||||
|
|
@ -815,6 +840,7 @@ async function saveProfile(event) {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
closeProfileModal();
|
||||||
await loadProfiles();
|
await loadProfiles();
|
||||||
resetForm();
|
resetForm();
|
||||||
showToast('Profile salvo.');
|
showToast('Profile salvo.');
|
||||||
|
|
|
||||||
|
|
@ -167,68 +167,16 @@
|
||||||
<div id="view-profiles" class="view hidden">
|
<div id="view-profiles" class="view hidden">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1 class="page-title">Backup Profiles</h1>
|
<h1 class="page-title">Backup Profiles</h1>
|
||||||
|
<div class="page-header-actions">
|
||||||
|
<button id="openCreateProfileModal" class="btn btn--primary">+ Criar Profile</button>
|
||||||
<button id="reloadProfiles" class="btn btn--secondary">Reload</button>
|
<button id="reloadProfiles" class="btn btn--secondary">Reload</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="profiles-layout">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-toolbar">
|
|
||||||
<h2 class="card-title">New Profile</h2>
|
|
||||||
<span id="formModeBadge" class="badge badge--accent">criar</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="profileForm" class="profile-form">
|
|
||||||
<input type="hidden" id="profileId" />
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="profileName">Nome do profile</label>
|
|
||||||
<input id="profileName" name="name" type="text" placeholder="Backup banco principal" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="backupDir">Diretório de backup no host Docker</label>
|
|
||||||
<input id="backupDir" name="backupDir" type="text" placeholder="/srv/docker-backups" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<fieldset class="form-fieldset">
|
|
||||||
<legend>Escopo do backup</legend>
|
|
||||||
<div class="radio-grid">
|
|
||||||
<label class="radio-card">
|
|
||||||
<input type="radio" name="backupScope" value="volumes" checked />
|
|
||||||
<span>Somente volumes</span>
|
|
||||||
<small>mantém o comportamento atual de backup de volumes e binds</small>
|
|
||||||
</label>
|
|
||||||
<label class="radio-card">
|
|
||||||
<input type="radio" name="backupScope" value="container" />
|
|
||||||
<span>Container inteiro</span>
|
|
||||||
<small>gera um único tar por container a partir de /</small>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<div class="container-picker">
|
|
||||||
<div class="picker-header">
|
|
||||||
<span class="picker-label">Containers</span>
|
|
||||||
<button id="refreshContainers" type="button" class="btn btn--ghost btn--sm">Atualizar lista</button>
|
|
||||||
</div>
|
|
||||||
<div id="containerOptions" class="container-options"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-actions">
|
|
||||||
<button class="btn btn--primary" type="submit">Salvar profile</button>
|
|
||||||
<button class="btn btn--secondary" type="button" id="clearForm">Limpar</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-toolbar">
|
|
||||||
<h2 class="card-title">Profiles Salvos</h2>
|
|
||||||
</div>
|
|
||||||
<div id="profilesList" class="profiles-list"></div>
|
<div id="profilesList" class="profiles-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- VIEW: Backup Runs -->
|
<!-- VIEW: Backup Runs -->
|
||||||
<div id="view-runs" class="view hidden">
|
<div id="view-runs" class="view hidden">
|
||||||
|
|
@ -318,6 +266,59 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="profileFormModal" class="modal hidden" aria-hidden="true">
|
||||||
|
<div class="modal-backdrop" data-action="close-profile-modal"></div>
|
||||||
|
<div class="modal-card modal-card--wide" role="dialog" aria-modal="true" aria-labelledby="profileModalTitle">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="profileModalTitle">Novo Profile</h3>
|
||||||
|
<button id="profileModalClose" class="btn btn--ghost btn--sm" type="button">Fechar</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="profileForm" class="profile-form">
|
||||||
|
<input type="hidden" id="profileId" />
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="profileName">Nome do profile</label>
|
||||||
|
<input id="profileName" name="name" type="text" placeholder="Backup banco principal" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="backupDir">Diretório de backup no host Docker</label>
|
||||||
|
<input id="backupDir" name="backupDir" type="text" placeholder="/srv/docker-backups" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="form-fieldset">
|
||||||
|
<legend>Escopo do backup</legend>
|
||||||
|
<div class="radio-grid">
|
||||||
|
<label class="radio-card">
|
||||||
|
<input type="radio" name="backupScope" value="volumes" checked />
|
||||||
|
<span>Somente volumes</span>
|
||||||
|
<small>mantém o comportamento atual de backup de volumes e binds</small>
|
||||||
|
</label>
|
||||||
|
<label class="radio-card">
|
||||||
|
<input type="radio" name="backupScope" value="container" />
|
||||||
|
<span>Container inteiro</span>
|
||||||
|
<small>gera um único tar por container a partir de /</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="container-picker">
|
||||||
|
<div class="picker-header">
|
||||||
|
<span class="picker-label">Containers</span>
|
||||||
|
<button id="refreshContainers" type="button" class="btn btn--ghost btn--sm">Atualizar lista</button>
|
||||||
|
</div>
|
||||||
|
<div id="containerOptions" class="container-options"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn--primary" type="submit">Salvar profile</button>
|
||||||
|
<button class="btn btn--secondary" type="button" id="clearForm">Cancelar</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/app.js"></script>
|
<script src="/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -146,6 +146,12 @@ button, input, select {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
@ -806,6 +812,11 @@ button, input, select {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-card--wide {
|
||||||
|
width: min(720px, calc(100vw - 24px));
|
||||||
|
max-height: min(90vh, 800px);
|
||||||
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
|
||||||
|
|
@ -510,6 +510,9 @@ class BackupService {
|
||||||
pushLog(`Arquivo gerado via helper: ${absoluteArchivePath}`, 'finalizando');
|
pushLog(`Arquivo gerado via helper: ${absoluteArchivePath}`, 'finalizando');
|
||||||
pushLog('Snapshot incremental salvo no diretorio de backup.', 'finalizando');
|
pushLog('Snapshot incremental salvo no diretorio de backup.', 'finalizando');
|
||||||
|
|
||||||
|
containerBackup.fileCount = Math.max(fileCurrent, fileTotal);
|
||||||
|
try { containerBackup.archiveSize = (await fs.stat(absoluteArchivePath)).size; } catch {}
|
||||||
|
|
||||||
onProgress({
|
onProgress({
|
||||||
containerName,
|
containerName,
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
|
|
@ -585,6 +588,9 @@ class BackupService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
containerBackup.fileCount = Math.max(fileCurrent, fileTotal);
|
||||||
|
try { containerBackup.archiveSize = (await fs.stat(absoluteArchivePath)).size; } catch {}
|
||||||
|
|
||||||
onProgress({
|
onProgress({
|
||||||
containerName,
|
containerName,
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
|
|
@ -655,6 +661,12 @@ class BackupService {
|
||||||
await this.dockerService.startContainer(containerId).catch(() => null);
|
await this.dockerService.startContainer(containerId).catch(() => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
containerBackup.fileCount = Math.max(fileCurrent, fileTotal);
|
||||||
|
try {
|
||||||
|
const hostArchivePath = path.join(profile.backupDir, ...archiveRelativePath.split('/'));
|
||||||
|
containerBackup.archiveSize = (await fs.stat(hostArchivePath)).size;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
onProgress({
|
onProgress({
|
||||||
containerName,
|
containerName,
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue