Modernize tenant selection page with improved UX
- Replace list view with responsive card grid layout for better scalability - Add real-time search functionality to filter tenants by name or ID - Move settings to fixed gear icon in top-right corner - Add new tenant button (plus icon) next to settings gear - Implement modal dialog for creating new tenants - Add hover effects and animations to tenant cards - Improve mobile responsiveness - Display tenant info more compactly (name, ID, username, date, AR URL) - Add "no results" message for empty search results The new layout handles 50+ tenants efficiently with better visual organization and easier navigation.
This commit is contained in:
@@ -6,10 +6,62 @@
|
|||||||
<title>Select or Create Tenant - KCAP Demo Server</title>
|
<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 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">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container mt-5">
|
<!-- Top Right Action Buttons -->
|
||||||
<h1 class="text-center mb-4">KCAP Demo Server - Multi-Tenant</h1>
|
<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) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
@@ -22,66 +74,101 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<div class="row justify-content-center">
|
<!-- Search and Filter -->
|
||||||
<div class="col-md-8">
|
<div class="search-container">
|
||||||
<div class="card">
|
<div class="row mb-3">
|
||||||
<div class="card-header">
|
<div class="col-md-8 offset-md-2">
|
||||||
<h3>Select an Existing Tenant</h3>
|
<div class="input-group">
|
||||||
</div>
|
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||||
<div class="card-body">
|
<input type="text" id="searchTenants" class="form-control" placeholder="Search tenants...">
|
||||||
{% if tenants %}
|
<button class="btn btn-outline-secondary" type="button" id="clearSearch">
|
||||||
<div class="list-group">
|
<i class="bi bi-x-lg"></i>
|
||||||
{% for tenant in tenants %}
|
|
||||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
||||||
<a href="/{{ tenant.id }}/" class="text-decoration-none flex-grow-1">
|
|
||||||
<div class="d-flex w-100 justify-content-between">
|
|
||||||
<h5 class="mb-1">{{ tenant.name }}</h5>
|
|
||||||
<small>{{ tenant.created_at }}</small>
|
|
||||||
</div>
|
|
||||||
<p class="mb-1">Username: {{ tenant.username }}</p>
|
|
||||||
<p class="mb-1">
|
|
||||||
<strong>Knox Capture AR Template URL:</strong>
|
|
||||||
<code>{{ server_url }}/{{ tenant.id }}/</code>
|
|
||||||
</p>
|
|
||||||
<small>ID: {{ tenant.id }}</small>
|
|
||||||
</a>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary me-2" data-bs-toggle="modal" data-bs-target="#qrModal-{{ tenant.id }}" onclick="event.stopPropagation();">
|
|
||||||
<i class="bi bi-qr-code"></i>
|
|
||||||
</button>
|
</button>
|
||||||
<form method="POST" action="/tenant/{{ tenant.id }}/delete" class="ms-3" onsubmit="return confirmDelete('{{ tenant.name }}')">
|
|
||||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
|
||||||
<p class="text-muted">No tenants exist yet. Create one below!</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mt-4">
|
<!-- Tenant Grid -->
|
||||||
<div class="card-header">
|
<div class="tenant-grid" id="tenantGrid">
|
||||||
<h3>Create a New Tenant</h3>
|
{% if tenants %}
|
||||||
</div>
|
{% 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="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>
|
||||||
|
<form method="POST" action="/tenant/{{ tenant.id }}/delete" class="d-inline" onsubmit="return confirmDelete('{{ tenant.name }}')">
|
||||||
|
<button type="submit" class="btn btn-danger btn-sm">
|
||||||
|
<i class="bi bi-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</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">
|
<form id="newTenantForm">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="tenantId" class="form-label">Tenant ID</label>
|
<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_-]+">
|
<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>
|
<small class="form-text text-muted">Only letters, numbers, hyphens, and underscores allowed</small>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Create & Enter Tenant</button>
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
<small>Default credentials: <strong>admin</strong> / <strong>admin</strong></small>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
<div class="mt-4 text-center">
|
<button type="submit" form="newTenantForm" class="btn btn-primary">Create & Enter</button>
|
||||||
<p class="text-muted">
|
|
||||||
<strong>Note:</strong> When you access a tenant URL directly (e.g., /my-tenant/),
|
|
||||||
it will be automatically created with default credentials (admin/admin).
|
|
||||||
</p>
|
|
||||||
<a href="/settings" class="btn btn-secondary mt-3">Server Settings</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,11 +180,11 @@
|
|||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="qrModalLabel-{{ tenant.id }}">AR Template URL QR Code - {{ tenant.name }}</h5>
|
<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>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body text-center">
|
<div class="modal-body text-center">
|
||||||
<p class="text-muted mb-3">Scan this QR code for the Knox Capture AR Template URL:</p>
|
<p class="text-muted mb-3">Scan this QR code for Knox Capture AR:</p>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<img src="/{{ tenant.id }}/qrcode/template" alt="AR Template QR Code" class="img-fluid" style="max-width: 300px;">
|
<img src="/{{ tenant.id }}/qrcode/template" alt="AR Template QR Code" class="img-fluid" style="max-width: 300px;">
|
||||||
</div>
|
</div>
|
||||||
@@ -117,21 +204,62 @@
|
|||||||
'images', 'barcodes', 'static', 'assets', 'js', 'css', 'tenant',
|
'images', 'barcodes', 'static', 'assets', 'js', 'css', 'tenant',
|
||||||
'auth', 'oauth', 'callback', 'webhook', 'health', 'status'];
|
'auth', 'oauth', 'callback', 'webhook', 'health', 'status'];
|
||||||
|
|
||||||
|
// Create tenant form submission
|
||||||
document.getElementById('newTenantForm').addEventListener('submit', function(e) {
|
document.getElementById('newTenantForm').addEventListener('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const tenantId = document.getElementById('tenantId').value;
|
const tenantId = document.getElementById('tenantId').value;
|
||||||
if (tenantId) {
|
if (tenantId) {
|
||||||
// Convert to lowercase for consistency
|
|
||||||
const normalizedId = tenantId.toLowerCase();
|
const normalizedId = tenantId.toLowerCase();
|
||||||
if (reservedIds.includes(normalizedId)) {
|
if (reservedIds.includes(normalizedId)) {
|
||||||
alert(`"${tenantId}" is a reserved name and cannot be used as a tenant ID.`);
|
alert(`"${tenantId}" is a reserved name and cannot be used as a tenant ID.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Use the normalized (lowercase) ID in the URL
|
|
||||||
window.location.href = '/' + normalizedId + '/';
|
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 confirmation
|
||||||
function confirmDelete(tenantName) {
|
function confirmDelete(tenantName) {
|
||||||
return confirm(`Are you sure you want to delete the tenant "${tenantName}"?\n\nThis will permanently delete all products and data associated with this tenant.`);
|
return confirm(`Are you sure you want to delete the tenant "${tenantName}"?\n\nThis will permanently delete all products and data associated with this tenant.`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user