Cloud Connect: clickable rows with shared modal (Inbox-style UX)
- Remove per-row Link/Create Job button and inline modals - Make unmatched rows clickable to open a single shared modal - Create new job tab: customer via datalist (auto-complete), job name and backup type shown read-only (derived from user/section) - Route: derive job_name and backup_type server-side, not from form Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4eac589625
commit
e3c7a5ddac
@ -117,8 +117,8 @@ def cloud_connect_account_link(cc_account_db_id: int):
|
|||||||
|
|
||||||
customer = Customer.query.get_or_404(customer_id)
|
customer = Customer.query.get_or_404(customer_id)
|
||||||
|
|
||||||
job_name = (request.form.get("job_name") or acc.user).strip()
|
job_name = acc.user.strip()
|
||||||
backup_type = (request.form.get("backup_type") or acc.derived_backup_type).strip()
|
backup_type = "Cloud Connect Agent" if acc.section == "Agent" else "Cloud Connect Backup"
|
||||||
|
|
||||||
job = Job(
|
job = Job(
|
||||||
customer_id=customer.id,
|
customer_id=customer.id,
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
{# ── Unmatched accounts ─────────────────────────────────────────────────── #}
|
{# ── Unmatched accounts ─────────────────────────────────────────────────── #}
|
||||||
{% if unmatched %}
|
{% if unmatched %}
|
||||||
<h4 class="mb-2">Unmatched <span class="badge bg-warning text-dark">{{ unmatched|length }}</span></h4>
|
<h4 class="mb-2">Unmatched <span class="badge bg-warning text-dark">{{ unmatched|length }}</span></h4>
|
||||||
<p class="text-muted small mb-3">These accounts have no linked job yet. Create a new job or link to an existing one.</p>
|
<p class="text-muted small mb-3">Click a row to create a new job or link to an existing one.</p>
|
||||||
|
|
||||||
<div class="table-responsive mb-4">
|
<div class="table-responsive mb-4">
|
||||||
<table class="table table-sm table-hover align-middle">
|
<table class="table table-sm table-hover align-middle">
|
||||||
@ -24,12 +24,16 @@
|
|||||||
<th>Last active</th>
|
<th>Last active</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>First seen</th>
|
<th>First seen</th>
|
||||||
<th></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for acc in unmatched %}
|
{% for acc in unmatched %}
|
||||||
<tr>
|
<tr class="cc-unmatched-row"
|
||||||
|
style="cursor: pointer;"
|
||||||
|
data-id="{{ acc.id }}"
|
||||||
|
data-user="{{ acc.user | e }}"
|
||||||
|
data-section="{{ acc.section | e }}"
|
||||||
|
data-backup-type="{{ acc.derived_backup_type | e }}">
|
||||||
<td class="fw-semibold">{{ acc.user }}</td>
|
<td class="fw-semibold">{{ acc.user }}</td>
|
||||||
<td><span class="badge bg-secondary">{{ acc.section }}</span></td>
|
<td><span class="badge bg-secondary">{{ acc.section }}</span></td>
|
||||||
<td class="text-muted small">{{ acc.repo_name or '—' }}<br><span class="text-muted" style="font-size:11px;">{{ acc.repo_type or '' }}</span></td>
|
<td class="text-muted small">{{ acc.repo_name or '—' }}<br><span class="text-muted" style="font-size:11px;">{{ acc.repo_type or '' }}</span></td>
|
||||||
@ -46,97 +50,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-muted small">{{ acc.first_seen_at|local_datetime }}</td>
|
<td class="text-muted small">{{ acc.first_seen_at|local_datetime }}</td>
|
||||||
<td>
|
|
||||||
<button class="btn btn-sm btn-primary"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#link-modal-{{ acc.id }}">
|
|
||||||
Link / Create job
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{# Link modal #}
|
|
||||||
<div class="modal fade" id="link-modal-{{ acc.id }}" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Link: {{ acc.user }} ({{ acc.section }})</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<ul class="nav nav-tabs mb-3" role="tablist">
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link active" data-bs-toggle="tab"
|
|
||||||
data-bs-target="#create-{{ acc.id }}" type="button">
|
|
||||||
Create new job
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link" data-bs-toggle="tab"
|
|
||||||
data-bs-target="#existing-{{ acc.id }}" type="button">
|
|
||||||
Link to existing job
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="tab-content">
|
|
||||||
{# Tab 1: Create new job #}
|
|
||||||
<div class="tab-pane fade show active" id="create-{{ acc.id }}">
|
|
||||||
<form method="post" action="{{ url_for('main.cloud_connect_account_link', cc_account_db_id=acc.id) }}">
|
|
||||||
<input type="hidden" name="action" value="create" />
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Customer <span class="text-danger">*</span></label>
|
|
||||||
<select class="form-select" name="customer_id" required>
|
|
||||||
<option value="">Select customer…</option>
|
|
||||||
{% for c in customers %}
|
|
||||||
<option value="{{ c.id }}">{{ c.name }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Job name</label>
|
|
||||||
<input type="text" class="form-control" name="job_name" value="{{ acc.user }}" />
|
|
||||||
<div class="form-text">Defaults to the user name from the report.</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Backup type</label>
|
|
||||||
<input type="text" class="form-control" name="backup_type"
|
|
||||||
value="{{ acc.derived_backup_type }}" />
|
|
||||||
<div class="form-text">Cloud Connect Backup or Cloud Connect Agent.</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-end gap-2">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
||||||
<button type="submit" class="btn btn-primary">Create job & link</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# Tab 2: Link to existing job #}
|
|
||||||
<div class="tab-pane fade" id="existing-{{ acc.id }}">
|
|
||||||
<form method="post" action="{{ url_for('main.cloud_connect_account_link', cc_account_db_id=acc.id) }}">
|
|
||||||
<input type="hidden" name="action" value="link" />
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Job <span class="text-danger">*</span></label>
|
|
||||||
<select class="form-select" name="job_id" required>
|
|
||||||
<option value="">Select job…</option>
|
|
||||||
{% for j in jobs %}
|
|
||||||
<option value="{{ j.id }}">
|
|
||||||
{{ j.customer.name ~ ' – ' if j.customer else '' }}{{ j.backup_software }} / {{ j.job_name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-end gap-2">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
||||||
<button type="submit" class="btn btn-primary">Link to job</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -193,7 +107,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{{ url_for('main.cloud_connect_account_unlink', cc_account_db_id=acc.id) }}"
|
action="{{ url_for('main.cloud_connect_account_unlink', cc_account_db_id=acc.id) }}"
|
||||||
onsubmit="return confirm('Remove link for {{ acc.user }} ({{ acc.section }})?');"
|
onsubmit="return confirm('Remove link for {{ acc.user | e }} ({{ acc.section | e }})?');"
|
||||||
class="mb-0">
|
class="mb-0">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-secondary">Unlink</button>
|
<button type="submit" class="btn btn-sm btn-outline-secondary">Unlink</button>
|
||||||
</form>
|
</form>
|
||||||
@ -211,4 +125,152 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{# ── Shared link/create modal ───────────────────────────────────────────── #}
|
||||||
|
<div class="modal fade" id="ccLinkModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="ccLinkModalTitle">Link account</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<ul class="nav nav-tabs mb-3" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" data-bs-toggle="tab"
|
||||||
|
data-bs-target="#ccTabCreate" type="button">
|
||||||
|
Create new job
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab"
|
||||||
|
data-bs-target="#ccTabExisting" type="button">
|
||||||
|
Link to existing job
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
{# Tab 1: Create new job #}
|
||||||
|
<div class="tab-pane fade show active" id="ccTabCreate">
|
||||||
|
<form id="ccCreateForm" method="post" action="">
|
||||||
|
<input type="hidden" name="action" value="create" />
|
||||||
|
<input type="hidden" id="ccCustomerId" name="customer_id" />
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Customer <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" id="ccCustomerInput" class="form-control"
|
||||||
|
list="ccCustomerList" placeholder="Select customer…"
|
||||||
|
autocomplete="off" />
|
||||||
|
<datalist id="ccCustomerList">
|
||||||
|
{% for c in customers %}
|
||||||
|
<option value="{{ c.name }}"></option>
|
||||||
|
{% endfor %}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="row mb-3">
|
||||||
|
<dt class="col-5">Backup software</dt>
|
||||||
|
<dd class="col-7">Veeam</dd>
|
||||||
|
<dt class="col-5">Backup type</dt>
|
||||||
|
<dd class="col-7" id="ccDisplayBackupType"></dd>
|
||||||
|
<dt class="col-5">Job name</dt>
|
||||||
|
<dd class="col-7" id="ccDisplayJobName"></dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end gap-2">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Create job & link</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Tab 2: Link to existing job #}
|
||||||
|
<div class="tab-pane fade" id="ccTabExisting">
|
||||||
|
<form id="ccLinkExistingForm" method="post" action="">
|
||||||
|
<input type="hidden" name="action" value="link" />
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Job <span class="text-danger">*</span></label>
|
||||||
|
<select class="form-select" name="job_id" required>
|
||||||
|
<option value="">Select job…</option>
|
||||||
|
{% for j in jobs %}
|
||||||
|
<option value="{{ j.id }}">
|
||||||
|
{{ j.customer.name ~ ' – ' if j.customer else '' }}{{ j.backup_software }} / {{ j.job_name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end gap-2">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Link to job</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var customers = {{ customers | tojson | safe }};
|
||||||
|
|
||||||
|
function findCustomerIdByName(name) {
|
||||||
|
for (var i = 0; i < customers.length; i++) {
|
||||||
|
if (customers[i].name === name) return customers[i].id;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachHandlers() {
|
||||||
|
var rows = document.querySelectorAll('.cc-unmatched-row');
|
||||||
|
var modalEl = document.getElementById('ccLinkModal');
|
||||||
|
if (!modalEl) return;
|
||||||
|
var modal = new bootstrap.Modal(modalEl);
|
||||||
|
|
||||||
|
var linkUrlTpl = "{{ url_for('main.cloud_connect_account_link', cc_account_db_id=0) }}";
|
||||||
|
|
||||||
|
rows.forEach(function (row) {
|
||||||
|
row.addEventListener('click', function () {
|
||||||
|
var id = row.getAttribute('data-id');
|
||||||
|
var user = row.getAttribute('data-user');
|
||||||
|
var section = row.getAttribute('data-section');
|
||||||
|
var backupType = row.getAttribute('data-backup-type');
|
||||||
|
var linkUrl = linkUrlTpl.replace('0', id);
|
||||||
|
|
||||||
|
document.getElementById('ccLinkModalTitle').textContent = user + ' (' + section + ')';
|
||||||
|
document.getElementById('ccDisplayBackupType').textContent = backupType;
|
||||||
|
document.getElementById('ccDisplayJobName').textContent = user;
|
||||||
|
|
||||||
|
var customerInput = document.getElementById('ccCustomerInput');
|
||||||
|
var customerIdField = document.getElementById('ccCustomerId');
|
||||||
|
if (customerInput) customerInput.value = '';
|
||||||
|
if (customerIdField) customerIdField.value = '';
|
||||||
|
|
||||||
|
var createForm = document.getElementById('ccCreateForm');
|
||||||
|
var linkForm = document.getElementById('ccLinkExistingForm');
|
||||||
|
|
||||||
|
if (createForm) {
|
||||||
|
createForm.action = linkUrl;
|
||||||
|
createForm.onsubmit = function (ev) {
|
||||||
|
var cid = findCustomerIdByName(customerInput ? customerInput.value : '');
|
||||||
|
if (!cid) {
|
||||||
|
ev.preventDefault();
|
||||||
|
alert('Please select an existing customer name from the list.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (customerIdField) customerIdField.value = String(cid);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (linkForm) linkForm.action = linkUrl;
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', attachHandlers);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
This file documents all changes made to this project via Claude Code.
|
This file documents all changes made to this project via Claude Code.
|
||||||
|
|
||||||
|
## [2026-03-20]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Cloud Connect Accounts page: replaced per-row "Link / Create Job" button and inline modals with clickable rows and a single shared modal (mirrors Inbox UX):
|
||||||
|
- Clicking an unmatched row opens a modal pre-filled with the account's user and section
|
||||||
|
- "Create new job" tab: customer via datalist input (auto-complete); Job name (from User) and Backup type (from Section) shown as read-only — not editable
|
||||||
|
- "Link to existing job" tab: unchanged
|
||||||
|
- `routes_cloud_connect.py`: job name and backup type are now derived server-side from `acc.user` and `acc.section` instead of reading from form fields
|
||||||
|
|
||||||
## [2026-03-19] (2)
|
## [2026-03-19] (2)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user