- Fixed incorrect route path from /tenant/{id}/delete to /{id}/delete
- Replaced browser confirm() dialog with Bootstrap modal for better UX
- Added styled confirmation modal with warning message
- Modal displays tenant name and ID before deletion
315 lines
15 KiB
HTML
315 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Select or Create Tenant - KCAP Demo Server</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
|
<style>
|
|
.tenant-card {
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
cursor: pointer;
|
|
height: 100%;
|
|
}
|
|
.tenant-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
}
|
|
.tenant-card-clickable {
|
|
text-decoration: none;
|
|
color: inherit;
|
|
display: block;
|
|
}
|
|
.settings-btn {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
z-index: 1000;
|
|
}
|
|
.search-container {
|
|
position: sticky;
|
|
top: 0;
|
|
background: white;
|
|
z-index: 100;
|
|
padding: 20px 0;
|
|
}
|
|
.tenant-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
.create-tenant-card {
|
|
border: 2px dashed #dee2e6;
|
|
background: #f8f9fa;
|
|
}
|
|
.create-tenant-card:hover {
|
|
border-color: #0d6efd;
|
|
background: #e7f1ff;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Top Right Action Buttons -->
|
|
<div class="settings-btn">
|
|
<button class="btn btn-primary me-2" data-bs-toggle="modal" data-bs-target="#createTenantModal" title="New Tenant">
|
|
<i class="bi bi-plus-circle-fill"></i>
|
|
</button>
|
|
<a href="/settings" class="btn btn-secondary" title="Server Settings">
|
|
<i class="bi bi-gear-fill"></i>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="container-fluid px-4 py-4">
|
|
<h1 class="text-center mb-4">KCAP Demo Server</h1>
|
|
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
{% for category, message in messages %}
|
|
<div class="alert alert-{{ 'success' if category == 'success' else 'danger' }} alert-dismissible fade show" role="alert">
|
|
{{ message }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
<!-- Search and Filter -->
|
|
<div class="search-container">
|
|
<div class="row mb-3">
|
|
<div class="col-md-8 offset-md-2">
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
|
<input type="text" id="searchTenants" class="form-control" placeholder="Search tenants...">
|
|
<button class="btn btn-outline-secondary" type="button" id="clearSearch">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tenant Grid -->
|
|
<div class="tenant-grid" id="tenantGrid">
|
|
{% if tenants %}
|
|
{% for tenant in tenants %}
|
|
<div class="tenant-item" data-tenant-name="{{ tenant.name|lower }}" data-tenant-id="{{ tenant.id|lower }}">
|
|
<div class="card tenant-card">
|
|
<a href="/{{ tenant.id }}/" class="tenant-card-clickable">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
<h5 class="card-title mb-0">{{ tenant.name }}</h5>
|
|
<span class="badge bg-secondary">{{ tenant.id }}</span>
|
|
</div>
|
|
<p class="card-text text-muted small mb-2">
|
|
<i class="bi bi-person"></i> {{ tenant.username }}
|
|
</p>
|
|
<p class="card-text text-muted small mb-2">
|
|
<i class="bi bi-calendar"></i> {{ tenant.created_at }}
|
|
</p>
|
|
<div class="mb-2">
|
|
<small class="text-muted">AR Template URL:</small>
|
|
<div class="input-group input-group-sm">
|
|
<input type="text" class="form-control form-control-sm" value="{{ server_url }}/{{ tenant.id }}/" readonly onclick="this.select()">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
<div class="card-footer bg-white d-flex justify-content-between" onclick="event.stopPropagation();">
|
|
<button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#qrModal-{{ tenant.id }}">
|
|
<i class="bi bi-qr-code"></i> QR
|
|
</button>
|
|
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteTenantModal" data-tenant-id="{{ tenant.id }}" data-tenant-name="{{ tenant.name }}">
|
|
<i class="bi bi-trash"></i> Delete
|
|
</button>
|
|
<form id="delete-tenant-form-{{ tenant.id }}" method="POST" action="/{{ tenant.id }}/delete" style="display: none;">
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="col-12">
|
|
<div class="text-center py-5">
|
|
<i class="bi bi-inbox" style="font-size: 4rem; color: #dee2e6;"></i>
|
|
<p class="text-muted mt-3">No tenants exist yet. Create your first tenant!</p>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- No Results Message -->
|
|
<div id="noResults" class="text-center py-5" style="display: none;">
|
|
<i class="bi bi-search" style="font-size: 4rem; color: #dee2e6;"></i>
|
|
<p class="text-muted mt-3">No tenants found matching your search.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Tenant Modal -->
|
|
<div class="modal fade" id="createTenantModal" tabindex="-1" aria-labelledby="createTenantModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="createTenantModalLabel">Create New Tenant</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="newTenantForm">
|
|
<div class="mb-3">
|
|
<label for="tenantId" class="form-label">Tenant ID</label>
|
|
<input type="text" class="form-control" id="tenantId" placeholder="e.g., demo-company" required pattern="[a-zA-Z0-9_-]+">
|
|
<small class="form-text text-muted">Only letters, numbers, hyphens, and underscores allowed</small>
|
|
</div>
|
|
<div class="alert alert-info">
|
|
<i class="bi bi-info-circle"></i>
|
|
<small>Default credentials: <strong>admin</strong> / <strong>admin</strong></small>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" form="newTenantForm" class="btn btn-primary">Create & Enter</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- QR Code Modals for each tenant -->
|
|
{% for tenant in tenants %}
|
|
<div class="modal fade" id="qrModal-{{ tenant.id }}" tabindex="-1" aria-labelledby="qrModalLabel-{{ tenant.id }}" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="qrModalLabel-{{ tenant.id }}">AR Template URL - {{ tenant.name }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body text-center">
|
|
<p class="text-muted mb-3">Scan this QR code for Knox Capture AR:</p>
|
|
<div class="mb-3">
|
|
<img src="/{{ tenant.id }}/qrcode/template" alt="AR Template QR Code" class="img-fluid" style="max-width: 300px;">
|
|
</div>
|
|
<code class="d-block mt-2">{{ server_url }}/{{ tenant.id }}/</code>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
<!-- Delete Tenant Confirmation Modal -->
|
|
<div class="modal fade" id="deleteTenantModal" tabindex="-1" aria-labelledby="deleteTenantModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-danger text-white">
|
|
<h5 class="modal-title" id="deleteTenantModalLabel">
|
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>Confirm Tenant Deletion
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="mb-3">Are you sure you want to delete the tenant <strong id="delete-tenant-name"></strong>?</p>
|
|
<div class="alert alert-danger mb-0">
|
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
|
<strong>Warning:</strong> This will permanently delete all products and data associated with this tenant. This action cannot be undone.
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-circle me-1"></i>Cancel
|
|
</button>
|
|
<button type="button" class="btn btn-danger" id="confirm-delete-tenant-btn">
|
|
<i class="bi bi-trash me-1"></i>Delete Tenant
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
const reservedIds = ['admin', 'api', 'login', 'logout', 'arcontentfields', 'arinfo',
|
|
'images', 'barcodes', 'static', 'assets', 'js', 'css', 'tenant',
|
|
'auth', 'oauth', 'callback', 'webhook', 'health', 'status'];
|
|
|
|
// Create tenant form submission
|
|
document.getElementById('newTenantForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const tenantId = document.getElementById('tenantId').value;
|
|
if (tenantId) {
|
|
const normalizedId = tenantId.toLowerCase();
|
|
if (reservedIds.includes(normalizedId)) {
|
|
alert(`"${tenantId}" is a reserved name and cannot be used as a tenant ID.`);
|
|
return;
|
|
}
|
|
window.location.href = '/' + normalizedId + '/';
|
|
}
|
|
});
|
|
|
|
// Search functionality
|
|
const searchInput = document.getElementById('searchTenants');
|
|
const clearButton = document.getElementById('clearSearch');
|
|
const tenantItems = document.querySelectorAll('.tenant-item');
|
|
const noResults = document.getElementById('noResults');
|
|
const tenantGrid = document.getElementById('tenantGrid');
|
|
|
|
function filterTenants() {
|
|
const searchTerm = searchInput.value.toLowerCase().trim();
|
|
let visibleCount = 0;
|
|
|
|
tenantItems.forEach(item => {
|
|
const tenantName = item.getAttribute('data-tenant-name');
|
|
const tenantId = item.getAttribute('data-tenant-id');
|
|
|
|
if (tenantName.includes(searchTerm) || tenantId.includes(searchTerm)) {
|
|
item.style.display = '';
|
|
visibleCount++;
|
|
} else {
|
|
item.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Show/hide no results message
|
|
if (visibleCount === 0 && searchTerm !== '') {
|
|
tenantGrid.style.display = 'none';
|
|
noResults.style.display = 'block';
|
|
} else {
|
|
tenantGrid.style.display = 'grid';
|
|
noResults.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
searchInput.addEventListener('input', filterTenants);
|
|
|
|
clearButton.addEventListener('click', function() {
|
|
searchInput.value = '';
|
|
filterTenants();
|
|
searchInput.focus();
|
|
});
|
|
|
|
// Delete tenant confirmation modal functionality
|
|
const deleteTenantModal = document.getElementById('deleteTenantModal');
|
|
if (deleteTenantModal) {
|
|
deleteTenantModal.addEventListener('show.bs.modal', function (event) {
|
|
// Button that triggered the modal
|
|
const button = event.relatedTarget;
|
|
|
|
// Extract tenant info from data attributes
|
|
const tenantId = button.getAttribute('data-tenant-id');
|
|
const tenantName = button.getAttribute('data-tenant-name');
|
|
|
|
// Update modal content
|
|
const modalTenantName = document.getElementById('delete-tenant-name');
|
|
modalTenantName.textContent = `"${tenantName}" (ID: ${tenantId})`;
|
|
|
|
// Setup the confirm button action
|
|
const confirmDeleteBtn = document.getElementById('confirm-delete-tenant-btn');
|
|
confirmDeleteBtn.onclick = function() {
|
|
document.getElementById('delete-tenant-form-' + tenantId).submit();
|
|
};
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |