feat: hash routing, provision modal, decommission/pause rename, clickable rows
Some checks failed
Build & Release / build (push) Has been cancelled
Some checks failed
Build & Release / build (push) Has been cancelled
This commit is contained in:
@@ -203,10 +203,6 @@
|
||||
<span class="nav-icon">◫</span>
|
||||
<span>Customers</span>
|
||||
</div>
|
||||
<div class="nav-item" id="nav-create-env" onclick="showPage('create-env')">
|
||||
<span class="nav-icon">+</span>
|
||||
<span>Create Env</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sidebar-user">
|
||||
<button class="user-btn" onclick="toggleUserMenu()">
|
||||
@@ -224,7 +220,10 @@
|
||||
|
||||
<!-- Customers list -->
|
||||
<div id="page-customers" class="page">
|
||||
<div class="page-title">Customers</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem">
|
||||
<div class="page-title" style="margin-bottom:0">Customers</div>
|
||||
<button class="btn btn-primary" onclick="openProvisionModal()">+ New Environment</button>
|
||||
</div>
|
||||
<div class="table-toolbar">
|
||||
<input class="search-input" id="customers-search" type="text" placeholder="Search customers…" oninput="onSearchInput()" />
|
||||
<button class="btn btn-slate" onclick="loadCustomers()">Refresh</button>
|
||||
@@ -263,7 +262,7 @@
|
||||
<!-- Customer Detail -->
|
||||
<div id="page-customer-detail" class="page">
|
||||
<div style="margin-bottom:1.2rem">
|
||||
<button class="btn btn-slate btn-sm" onclick="showPage('customers')">← Customers</button>
|
||||
<button class="btn btn-slate btn-sm" onclick="navigate('customers')">← Customers</button>
|
||||
</div>
|
||||
<div class="detail-header">
|
||||
<div style="flex:1">
|
||||
@@ -273,7 +272,7 @@
|
||||
</div>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center">
|
||||
<button class="btn btn-slate btn-sm" onclick="refreshDetail()">↻ Refresh Status</button>
|
||||
<button class="btn btn-danger btn-sm" id="detail-delete-btn" onclick="">Delete</button>
|
||||
<button class="btn btn-danger btn-sm" id="detail-delete-btn" onclick="">Decommission</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-grid" id="detail-grid">
|
||||
@@ -284,10 +283,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Env -->
|
||||
<div id="page-create-env" class="page">
|
||||
<div class="page-title">Create Environment</div>
|
||||
|
||||
<!-- Account -->
|
||||
<div id="page-account" class="page">
|
||||
<div class="page-title">Account</div>
|
||||
<div class="card">
|
||||
<div class="form-group">
|
||||
<label for="new-password">New Password</label>
|
||||
<input id="new-password" type="password" autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm-password">Confirm Password</label>
|
||||
<input id="confirm-password" type="password" autocomplete="new-password" />
|
||||
</div>
|
||||
<button class="btn btn-slate" onclick="changePassword()">Update Password</button>
|
||||
<div id="account-status" class="status"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kebab dropdown (shared, repositioned via JS) -->
|
||||
<div id="kebab-menu">
|
||||
<div class="kebab-item" onclick="kebabAction('view')">View Details</div>
|
||||
<div class="kebab-item" id="kebab-deactivate" onclick="kebabAction('deactivate')">Pause</div>
|
||||
<div class="kebab-item" id="kebab-reactivate" onclick="kebabAction('reactivate')" style="display:none">Resume</div>
|
||||
<div class="kebab-item" onclick="kebabAction('upgrade')">Upgrade Chart</div>
|
||||
<div class="kebab-item danger" onclick="kebabAction('delete')">Decommission</div>
|
||||
<div class="kebab-item" onclick="kebabAction('record')">Remove Record Only</div>
|
||||
</div>
|
||||
|
||||
<!-- Decommission confirm dialog -->
|
||||
<div class="overlay" id="delete-overlay" onclick="if(event.target===this)closeDeleteDialog()">
|
||||
<div class="dialog">
|
||||
<h3>Decommission Customer</h3>
|
||||
<p>This will permanently remove the deployment, database, storage, and all associated resources for <strong id="delete-slug-label" style="color:#e6edf3"></strong>. This cannot be undone.</p>
|
||||
<div class="dialog-actions">
|
||||
<button class="btn btn-slate" onclick="closeDeleteDialog()">Cancel</button>
|
||||
<button class="btn btn-danger" id="delete-confirm-btn" onclick="confirmDelete()">Decommission</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Provision modal -->
|
||||
<div class="overlay" id="provision-overlay" onclick="if(event.target===this)closeProvisionModal()">
|
||||
<div class="dialog" style="max-width:480px;width:100%">
|
||||
<h3>New Environment</h3>
|
||||
<div class="form-group">
|
||||
<label for="customer-name">Customer Name</label>
|
||||
<input id="customer-name" type="text" placeholder="Acme Shop" />
|
||||
@@ -317,50 +360,10 @@
|
||||
<label class="module-check" id="mod-accounting"><input type="checkbox" value="accounting" onchange="toggleModule(this)" /> Accounting</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="provision-btn" onclick="provision()" style="margin-top:0.4rem">Provision</button>
|
||||
<div id="provision-status" class="status"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account -->
|
||||
<div id="page-account" class="page">
|
||||
<div class="page-title">Account</div>
|
||||
<div class="card">
|
||||
<div class="form-group">
|
||||
<label for="new-password">New Password</label>
|
||||
<input id="new-password" type="password" autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm-password">Confirm Password</label>
|
||||
<input id="confirm-password" type="password" autocomplete="new-password" />
|
||||
</div>
|
||||
<button class="btn btn-slate" onclick="changePassword()">Update Password</button>
|
||||
<div id="account-status" class="status"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kebab dropdown (shared, repositioned via JS) -->
|
||||
<div id="kebab-menu">
|
||||
<div class="kebab-item" onclick="kebabAction('view')">View Details</div>
|
||||
<div class="kebab-item" id="kebab-deactivate" onclick="kebabAction('deactivate')">Deactivate</div>
|
||||
<div class="kebab-item" id="kebab-reactivate" onclick="kebabAction('reactivate')" style="display:none">Reactivate</div>
|
||||
<div class="kebab-item" onclick="kebabAction('upgrade')">Upgrade Chart</div>
|
||||
<div class="kebab-item danger" onclick="kebabAction('delete')">Delete</div>
|
||||
<div class="kebab-item" onclick="kebabAction('record')">Remove Record Only</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete confirm dialog -->
|
||||
<div class="overlay" id="delete-overlay" onclick="if(event.target===this)closeDeleteDialog()">
|
||||
<div class="dialog">
|
||||
<h3>Delete Customer</h3>
|
||||
<p>This will remove the ArgoCD app, database, storage, and all associated resources for <strong id="delete-slug-label" style="color:#e6edf3"></strong>. This cannot be undone.</p>
|
||||
<div class="dialog-actions">
|
||||
<button class="btn btn-slate" onclick="closeDeleteDialog()">Cancel</button>
|
||||
<button class="btn btn-danger" id="delete-confirm-btn" onclick="confirmDelete()">Delete</button>
|
||||
<button class="btn btn-slate" onclick="closeProvisionModal()">Cancel</button>
|
||||
<button class="btn btn-primary" id="provision-btn" onclick="provision()">Provision</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -389,7 +392,7 @@
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setUser(data.username);
|
||||
showPage('customers');
|
||||
handleRoute();
|
||||
loadCustomers();
|
||||
} else {
|
||||
localStorage.removeItem('token');
|
||||
@@ -424,7 +427,7 @@
|
||||
localStorage.setItem('token', data.token);
|
||||
setUser(data.username);
|
||||
document.getElementById('login-view').style.display = 'none';
|
||||
showPage('customers');
|
||||
handleRoute();
|
||||
loadCustomers();
|
||||
} catch (err) {
|
||||
status.textContent = err.message;
|
||||
@@ -445,6 +448,10 @@
|
||||
|
||||
// ── Navigation ────────────────────────────────────────────────────────────
|
||||
|
||||
function navigate(path) {
|
||||
window.location.hash = path;
|
||||
}
|
||||
|
||||
function showPage(name) {
|
||||
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
||||
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
||||
@@ -455,6 +462,23 @@
|
||||
closeKebab();
|
||||
}
|
||||
|
||||
function handleRoute() {
|
||||
const hash = window.location.hash.slice(1) || 'customers';
|
||||
if (hash.startsWith('customers/')) {
|
||||
const slug = hash.slice('customers/'.length);
|
||||
showPage('customers');
|
||||
openDetail(slug);
|
||||
} else if (hash === 'customers') {
|
||||
showPage('customers');
|
||||
} else if (hash === 'account') {
|
||||
showPage('account');
|
||||
} else {
|
||||
showPage('customers');
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', handleRoute);
|
||||
|
||||
// ── Customers table ───────────────────────────────────────────────────────
|
||||
|
||||
async function loadCustomers() {
|
||||
@@ -496,7 +520,7 @@
|
||||
const expiry = r.expiration_date
|
||||
? `<span class="${expired ? 'tag tag-expired' : ''}">${fmtDate(r.expiration_date)}</span>`
|
||||
: '<span style="color:#484f58">—</span>';
|
||||
return `<tr style="cursor:pointer" ondblclick="openDetail('${r.slug}')">
|
||||
return `<tr style="cursor:pointer" onclick="navigate('customers/${r.slug}')">
|
||||
<td style="font-weight:500;color:#e6edf3">${r.slug}</td>
|
||||
<td>${r.name || '<span style="color:#484f58">—</span>'}</td>
|
||||
<td>${customerStatusBadge(r)}</td>
|
||||
@@ -610,6 +634,16 @@
|
||||
loadDetail(slug, false);
|
||||
}
|
||||
|
||||
function openProvisionModal() {
|
||||
document.getElementById('provision-status').className = 'status';
|
||||
document.getElementById('provision-status').textContent = '';
|
||||
document.getElementById('provision-overlay').classList.add('open');
|
||||
}
|
||||
|
||||
function closeProvisionModal() {
|
||||
document.getElementById('provision-overlay').classList.remove('open');
|
||||
}
|
||||
|
||||
function refreshDetail() {
|
||||
if (currentDetailSlug) loadDetail(currentDetailSlug, true);
|
||||
}
|
||||
@@ -803,18 +837,18 @@
|
||||
throw new Error(d.message ?? 'Delete failed');
|
||||
}
|
||||
closeDeleteDialog();
|
||||
if (currentDetailSlug === slug) showPage('customers');
|
||||
if (currentDetailSlug === slug) navigate('customers');
|
||||
loadCustomers();
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Delete';
|
||||
btn.textContent = 'Decommission';
|
||||
}
|
||||
}
|
||||
|
||||
async function deactivate(slug) {
|
||||
if (!confirm(`Deactivate "${slug}"?\n\nThis removes the deployment but keeps the database and storage. You can reactivate later.`)) return;
|
||||
if (!confirm(`Pause "${slug}"?\n\nThis removes the deployment but keeps the database and storage. You can resume later.`)) return;
|
||||
const res = await apiFetch(`/api/customers/${slug}/deactivate`, { method: 'POST' });
|
||||
if (res.ok) { loadCustomers(); if (currentDetailSlug === slug) loadDetail(slug, false); }
|
||||
else { const d = await res.json().catch(() => ({})); alert(d.message ?? 'Failed'); }
|
||||
@@ -831,7 +865,7 @@
|
||||
if (!confirm(`Remove "${slug}" from the manager database only?\n\nThis will NOT delete the ArgoCD app or DO database.`)) return;
|
||||
const res = await apiFetch(`/api/customers/${slug}/record`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
if (currentDetailSlug === slug) showPage('customers');
|
||||
if (currentDetailSlug === slug) navigate('customers');
|
||||
loadCustomers();
|
||||
} else {
|
||||
const d = await res.json().catch(() => ({}));
|
||||
@@ -902,12 +936,12 @@
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.message ?? 'Unknown error');
|
||||
status.textContent = `✓ ${data.slug} provisioned successfully`;
|
||||
status.className = 'status success';
|
||||
closeProvisionModal();
|
||||
document.getElementById('customer-name').value = '';
|
||||
document.getElementById('slug').value = '';
|
||||
document.getElementById('expiration-date').value = '';
|
||||
document.querySelectorAll('.module-check').forEach(l => { l.classList.remove('checked'); l.querySelector('input').checked = false; });
|
||||
loadCustomers();
|
||||
} catch (err) {
|
||||
status.textContent = `✗ ${err.message}`;
|
||||
status.className = 'status error';
|
||||
|
||||
Reference in New Issue
Block a user