mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-10-16 11:08:15 +02:00
feat: Enhance user management and dashboard components
- Updated user management templates to improve user experience with a new layout and success/error message handling. - Implemented password hashing for user creation and management, enhancing security. - Refactored dashboard components to include better data presentation and responsiveness. - Added dark mode support for various components, ensuring a consistent user interface across themes. - Improved toast notification animations for better user feedback during actions.
This commit is contained in:
1 parent
3140d9e143
commit
30d0725331
13 files changed
+1656
-761
No files matched your search
@@ -262,7 +262,7 @@ templ AdminDatabaseTools(ctx context.Context, backups []BackupFile) {
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.id = 'toast-' + type + '-' + Date.now();
|
||||
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800';
|
||||
toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out';
|
||||
toast.role = 'alert';
|
||||
|
||||
// Set toast content based on type
|
||||
@@ -301,15 +301,25 @@ templ AdminDatabaseTools(ctx context.Context, backups []BackupFile) {
|
||||
// Add toast to container
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// Trigger animation after a small delay to ensure the DOM has updated
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-y-16', 'opacity-0');
|
||||
toast.classList.add('translate-y-0', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
// Add event listener to close button
|
||||
const closeButton = toast.querySelector('button[data-dismiss-target]');
|
||||
closeButton.addEventListener('click', function() {
|
||||
toast.remove();
|
||||
// Animate out before removing
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Auto-remove toast after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.add('opacity-0', 'transition-opacity', 'duration-300');
|
||||
toast.classList.add('opacity-0', 'translate-y-4');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
|
||||
@@ -94,14 +94,20 @@ templ AdminRoles(ctx context.Context, data RolesData) {
|
||||
<div class="p-4 md:p-5 text-center">
|
||||
<i class="fas fa-exclamation-triangle mx-auto mb-4 text-red-400 text-3xl"></i>
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">Are you sure you want to delete this role?</h3>
|
||||
<form method="POST" id="delete-form" action="">
|
||||
<button type="submit" class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2">
|
||||
<div>
|
||||
<button
|
||||
id="confirm-delete-button"
|
||||
type="button"
|
||||
class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2">
|
||||
Yes, delete it
|
||||
</button>
|
||||
<button type="button" data-modal-hide="confirm-delete-modal" class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||
<button
|
||||
type="button"
|
||||
data-modal-hide="confirm-delete-modal"
|
||||
class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
|
||||
No, cancel
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,14 +115,52 @@ templ AdminRoles(ctx context.Context, data RolesData) {
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let currentRoleId = null;
|
||||
const modal = document.getElementById('confirm-delete-modal');
|
||||
|
||||
// Handle delete modal
|
||||
const deleteButtons = document.querySelectorAll('[data-modal-target="confirm-delete-modal"]');
|
||||
deleteButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const roleId = this.getAttribute('data-role-id');
|
||||
document.getElementById('delete-form').action = `/admin/roles/${roleId}/delete`;
|
||||
currentRoleId = this.getAttribute('data-role-id');
|
||||
});
|
||||
});
|
||||
|
||||
// Handle confirm delete button
|
||||
const confirmDeleteButton = document.getElementById('confirm-delete-button');
|
||||
confirmDeleteButton.addEventListener('click', function() {
|
||||
if (currentRoleId) {
|
||||
// Use fetch to send DELETE request
|
||||
fetch(`/admin/roles/${currentRoleId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
// Reload page on success
|
||||
window.location.reload();
|
||||
} else {
|
||||
// Handle error
|
||||
response.json().then(data => {
|
||||
alert('Error: ' + (data.error || 'Failed to delete role'));
|
||||
}).catch(() => {
|
||||
alert('An error occurred while deleting the role');
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('An error occurred while deleting the role');
|
||||
})
|
||||
.finally(() => {
|
||||
// Hide modal
|
||||
const closeModalButton = document.querySelector('[data-modal-hide="confirm-delete-modal"]');
|
||||
if (closeModalButton) closeModalButton.click();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
+223
-201
@@ -43,214 +43,216 @@ func GetRcloneVersion() string {
|
||||
|
||||
templ Dashboard(ctx context.Context, data DashboardData) {
|
||||
@LayoutWithContext("Dashboard", ctx) {
|
||||
<div class="p-4 md:p-6 2xl:p-10">
|
||||
<!-- Page Header -->
|
||||
<div class="flex flex-col md:flex-row items-start md:items-center justify-between mb-6 md:mb-8 gap-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-chart-pie w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||
Dashboard
|
||||
</h1>
|
||||
<div class="bg-white dark:bg-gray-800 p-2 px-4 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<span id="current-date" class="font-medium text-gray-700 dark:text-gray-300"></span>
|
||||
<script>
|
||||
document.getElementById('current-date').textContent = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Overview Cards -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||
<!-- Active Transfers -->
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Active Transfers</h3>
|
||||
<div class="inline-flex items-center justify-center w-10 h-10 text-blue-600 bg-blue-100 rounded-lg dark:text-blue-300 dark:bg-blue-900">
|
||||
<i class="fas fa-exchange-alt w-6 h-6 flex items-center justify-center"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<span class="text-4xl font-bold leading-none text-gray-900 dark:text-white">{ strconv.Itoa(data.ActiveTransfers) }</span>
|
||||
</div>
|
||||
<div class="ml-3 flex items-baseline text-sm font-medium">
|
||||
<span class="text-gray-500 dark:text-gray-400">Currently in progress</span>
|
||||
</div>
|
||||
<div id="dashboard-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||
<div class="pb-8 w-full">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-chart-pie w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
||||
Dashboard
|
||||
</h1>
|
||||
<div class="bg-white dark:bg-gray-800 p-2 px-4 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<span id="current-date" class="font-medium text-gray-700 dark:text-gray-300"></span>
|
||||
<script>
|
||||
document.getElementById('current-date').textContent = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Completed Today -->
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Completed Today</h3>
|
||||
<div class="inline-flex items-center justify-center w-10 h-10 text-green-600 bg-green-100 rounded-lg dark:text-green-300 dark:bg-green-900">
|
||||
<i class="fas fa-check-circle w-6 h-6 flex items-center justify-center"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<span class="text-4xl font-bold leading-none text-gray-900 dark:text-white">{ strconv.Itoa(data.CompletedToday) }</span>
|
||||
</div>
|
||||
<div class="ml-3 flex items-baseline text-sm font-medium">
|
||||
<span class="text-gray-500 dark:text-gray-400">Successfully completed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Failed Transfers -->
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Failed Transfers</h3>
|
||||
<div class="inline-flex items-center justify-center w-10 h-10 text-red-600 bg-red-100 rounded-lg dark:text-red-300 dark:bg-red-900">
|
||||
<i class="fas fa-exclamation-circle w-6 h-6 flex items-center justify-center"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<span class="text-4xl font-bold leading-none text-gray-900 dark:text-white">{ strconv.Itoa(data.FailedTransfers) }</span>
|
||||
</div>
|
||||
<div class="ml-3 flex items-baseline text-sm font-medium">
|
||||
<span class="text-gray-500 dark:text-gray-400">Require attention</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
||||
<!-- Recent Jobs Card -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between px-4 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-history w-6 h-6 mr-2 text-blue-500 dark:text-blue-400 flex-shrink-0"></i>
|
||||
Recent Jobs
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
if len(data.RecentJobs) == 0 {
|
||||
<!-- Empty state with Flowbite styling -->
|
||||
<div class="flex flex-col items-center justify-center py-8 px-4 text-center">
|
||||
<div class="inline-flex h-16 w-16 flex-shrink-0 items-center justify-center rounded-full bg-gray-100 mb-4 dark:bg-gray-700">
|
||||
<i class="fas fa-clipboard-list text-gray-500 dark:text-gray-400 text-3xl"></i>
|
||||
</div>
|
||||
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">No recent jobs found</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-4">Get started by creating your first job.</p>
|
||||
<a href="/jobs/new" class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||
Create your first job
|
||||
</a>
|
||||
<!-- Stats Overview Cards -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||
<!-- Active Transfers -->
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Active Transfers</h3>
|
||||
<div class="inline-flex items-center justify-center w-10 h-10 text-blue-600 bg-blue-100 rounded-lg dark:text-blue-300 dark:bg-blue-900">
|
||||
<i class="fas fa-exchange-alt w-6 h-6 flex items-center justify-center"></i>
|
||||
</div>
|
||||
} else {
|
||||
<div class="flow-root">
|
||||
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
for _, job := range data.RecentJobs {
|
||||
<li class="py-3 sm:py-4 hover:bg-gray-50 dark:hover:bg-gray-700 px-3 rounded-lg transition-colors">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
if job.Status == "completed" {
|
||||
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full text-green-600 bg-green-100 dark:bg-green-900">
|
||||
<i class="fas fa-check-circle w-6 h-6 flex items-center justify-center"></i>
|
||||
</span>
|
||||
} else if job.Status == "failed" {
|
||||
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full text-red-600 bg-red-100 dark:bg-red-900">
|
||||
<i class="fas fa-exclamation-circle w-6 h-6 flex items-center justify-center"></i>
|
||||
</span>
|
||||
} else {
|
||||
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full text-blue-600 bg-blue-100 dark:bg-blue-900">
|
||||
<i class="fas fa-tasks w-6 h-6 flex items-center justify-center"></i>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 ms-4">
|
||||
<p class="text-sm font-medium text-gray-900 truncate dark:text-white">
|
||||
{ getConfigNameForHistory(job, data.Configs) }
|
||||
</p>
|
||||
<div class="flex items-center mt-1">
|
||||
<i class="fas fa-clock w-4 h-4 text-gray-500 dark:text-gray-400 mr-1"></i>
|
||||
<p class="text-sm text-gray-500 truncate dark:text-gray-400">
|
||||
Started: { job.StartTime.Format("Jan 02, 2006 15:04:05") }
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<span class="text-4xl font-bold leading-none text-gray-900 dark:text-white">{ strconv.Itoa(data.ActiveTransfers) }</span>
|
||||
</div>
|
||||
<div class="ml-3 flex items-baseline text-sm font-medium">
|
||||
<span class="text-gray-500 dark:text-gray-400">Currently in progress</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Completed Today -->
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Completed Today</h3>
|
||||
<div class="inline-flex items-center justify-center w-10 h-10 text-green-600 bg-green-100 rounded-lg dark:text-green-300 dark:bg-green-900">
|
||||
<i class="fas fa-check-circle w-6 h-6 flex items-center justify-center"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<span class="text-4xl font-bold leading-none text-gray-900 dark:text-white">{ strconv.Itoa(data.CompletedToday) }</span>
|
||||
</div>
|
||||
<div class="ml-3 flex items-baseline text-sm font-medium">
|
||||
<span class="text-gray-500 dark:text-gray-400">Successfully completed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Failed Transfers -->
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Failed Transfers</h3>
|
||||
<div class="inline-flex items-center justify-center w-10 h-10 text-red-600 bg-red-100 rounded-lg dark:text-red-300 dark:bg-red-900">
|
||||
<i class="fas fa-exclamation-circle w-6 h-6 flex items-center justify-center"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<span class="text-4xl font-bold leading-none text-gray-900 dark:text-white">{ strconv.Itoa(data.FailedTransfers) }</span>
|
||||
</div>
|
||||
<div class="ml-3 flex items-baseline text-sm font-medium">
|
||||
<span class="text-gray-500 dark:text-gray-400">Require attention</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
||||
<!-- Recent Jobs Card -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between px-4 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-history w-6 h-6 mr-2 text-blue-500 dark:text-blue-400 flex-shrink-0"></i>
|
||||
Recent Jobs
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
if len(data.RecentJobs) == 0 {
|
||||
<!-- Empty state with Flowbite styling -->
|
||||
<div class="flex flex-col items-center justify-center py-8 px-4 text-center">
|
||||
<div class="inline-flex h-16 w-16 flex-shrink-0 items-center justify-center rounded-full bg-gray-100 mb-4 dark:bg-gray-700">
|
||||
<i class="fas fa-clipboard-list text-gray-500 dark:text-gray-400 text-3xl"></i>
|
||||
</div>
|
||||
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">No recent jobs found</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-4">Get started by creating your first job.</p>
|
||||
<a href="/jobs/new" class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||
<i class="fas fa-plus w-4 h-4 mr-2"></i>
|
||||
Create your first job
|
||||
</a>
|
||||
</div>
|
||||
} else {
|
||||
<div class="flow-root">
|
||||
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
for _, job := range data.RecentJobs {
|
||||
<li class="py-3 sm:py-4 hover:bg-gray-50 dark:hover:bg-gray-700 px-3 rounded-lg transition-colors">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
if job.Status == "completed" {
|
||||
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full text-green-600 bg-green-100 dark:bg-green-900">
|
||||
<i class="fas fa-check-circle w-6 h-6 flex items-center justify-center"></i>
|
||||
</span>
|
||||
} else if job.Status == "failed" {
|
||||
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full text-red-600 bg-red-100 dark:bg-red-900">
|
||||
<i class="fas fa-exclamation-circle w-6 h-6 flex items-center justify-center"></i>
|
||||
</span>
|
||||
} else {
|
||||
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full text-blue-600 bg-blue-100 dark:bg-blue-900">
|
||||
<i class="fas fa-tasks w-6 h-6 flex items-center justify-center"></i>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 ms-4">
|
||||
<p class="text-sm font-medium text-gray-900 truncate dark:text-white">
|
||||
{ getConfigNameForHistory(job, data.Configs) }
|
||||
</p>
|
||||
<div class="flex items-center mt-1">
|
||||
<i class="fas fa-clock w-4 h-4 text-gray-500 dark:text-gray-400 mr-1"></i>
|
||||
<p class="text-sm text-gray-500 truncate dark:text-gray-400">
|
||||
Started: { job.StartTime.Format("Jan 02, 2006 15:04:05") }
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("/job-runs/%d", job.ID)) }
|
||||
class="inline-flex items-center text-sm font-medium text-blue-600 hover:underline dark:text-blue-500">
|
||||
View
|
||||
<i class="fas fa-arrow-right w-3 h-3 ms-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("/job-runs/%d", job.ID)) }
|
||||
class="inline-flex items-center text-sm font-medium text-blue-600 hover:underline dark:text-blue-500">
|
||||
View
|
||||
<i class="fas fa-arrow-right w-3 h-3 ms-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pt-4 border-t border-gray-200 dark:border-gray-700 mt-4">
|
||||
<a href="/jobs" class="inline-flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
|
||||
<i class="fas fa-list w-4 h-4 mr-2"></i>
|
||||
View all jobs
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pt-4 border-t border-gray-200 dark:border-gray-700 mt-4">
|
||||
<a href="/jobs" class="inline-flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
|
||||
<i class="fas fa-list w-4 h-4 mr-2"></i>
|
||||
View all jobs
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions Card -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between px-4 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-bolt w-6 h-6 mr-2 text-blue-500 dark:text-blue-400 flex-shrink-0"></i>
|
||||
Quick Actions
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-4 space-y-4">
|
||||
<a href="/configs/new" class="inline-flex items-center justify-center w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-plus w-5 h-5 mr-2"></i>
|
||||
Create New Config
|
||||
</a>
|
||||
<a href="/jobs/new" class="inline-flex items-center justify-center w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-tasks w-5 h-5 mr-2"></i>
|
||||
Create New Job
|
||||
</a>
|
||||
<a href="/history" class="inline-flex items-center justify-center w-full text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700">
|
||||
<i class="fas fa-history w-5 h-5 mr-2"></i>
|
||||
View Transfer History
|
||||
</a>
|
||||
|
||||
<!-- System Status Card with Flowbite design -->
|
||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm mt-6">
|
||||
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 rounded-t-lg border-b border-gray-200 dark:border-gray-600">
|
||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white">System Status</h4>
|
||||
</div>
|
||||
<div class="px-4 py-3">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Server</span>
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
||||
<span class="w-2 h-2 mr-1 bg-green-500 rounded-full"></span>
|
||||
Online
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Scheduler</span>
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
||||
<span class="w-2 h-2 mr-1 bg-green-500 rounded-full"></span>
|
||||
Running
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Database</span>
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
||||
<span class="w-2 h-2 mr-1 bg-green-500 rounded-full"></span>
|
||||
Connected
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Rclone</span>
|
||||
<span class="bg-blue-100 text-blue-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-blue-900 dark:text-blue-300">
|
||||
<i class="fas fa-terminal w-3 h-3 mr-1"></i>
|
||||
if data.RcloneVersion != "" {
|
||||
{ data.RcloneVersion }
|
||||
} else {
|
||||
Unknown
|
||||
}
|
||||
</span>
|
||||
|
||||
<!-- Quick Actions Card -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between px-4 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-bolt w-6 h-6 mr-2 text-blue-500 dark:text-blue-400 flex-shrink-0"></i>
|
||||
Quick Actions
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-4 space-y-4">
|
||||
<a href="/configs/new" class="inline-flex items-center justify-center w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-plus w-5 h-5 mr-2"></i>
|
||||
Create New Config
|
||||
</a>
|
||||
<a href="/jobs/new" class="inline-flex items-center justify-center w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-tasks w-5 h-5 mr-2"></i>
|
||||
Create New Job
|
||||
</a>
|
||||
<a href="/history" class="inline-flex items-center justify-center w-full text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700">
|
||||
<i class="fas fa-history w-5 h-5 mr-2"></i>
|
||||
View Transfer History
|
||||
</a>
|
||||
|
||||
<!-- System Status Card with Flowbite design -->
|
||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm mt-6">
|
||||
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 rounded-t-lg border-b border-gray-200 dark:border-gray-600">
|
||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white">System Status</h4>
|
||||
</div>
|
||||
<div class="px-4 py-3">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Server</span>
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
||||
<span class="w-2 h-2 mr-1 bg-green-500 rounded-full"></span>
|
||||
Online
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Scheduler</span>
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
||||
<span class="w-2 h-2 mr-1 bg-green-500 rounded-full"></span>
|
||||
Running
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Database</span>
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
||||
<span class="w-2 h-2 mr-1 bg-green-500 rounded-full"></span>
|
||||
Connected
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Rclone</span>
|
||||
<span class="bg-blue-100 text-blue-800 text-xs font-medium inline-flex items-center px-2.5 py-0.5 rounded-full dark:bg-blue-900 dark:text-blue-300">
|
||||
<i class="fas fa-terminal w-3 h-3 mr-1"></i>
|
||||
if data.RcloneVersion != "" {
|
||||
{ data.RcloneVersion }
|
||||
} else {
|
||||
Unknown
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -258,6 +260,26 @@ templ Dashboard(ctx context.Context, data DashboardData) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Set dark background color if in dark mode
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
document.getElementById('dashboard-container').style.backgroundColor = '#111827';
|
||||
}
|
||||
|
||||
// Add event listener for theme changes
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
setTimeout(function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
document.getElementById('dashboard-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
+343
-273
@@ -341,17 +341,15 @@ templ FileMetadataListPartial(data FileMetadataListData) {
|
||||
// Update the main FileMetadataList template to use the partial template
|
||||
templ FileMetadataList(ctx context.Context, data FileMetadataListData) {
|
||||
@LayoutWithContext("File Metadata", ctx) {
|
||||
<div class="px-2 py-4 sm:px-4 w-full">
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<div id="file-metadata-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||
<div class="pb-8 w-full">
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-file-alt w-6 h-6 mr-2 text-blue-500"></i>
|
||||
if data.Job != nil {
|
||||
Files for Job: { data.Job.Name }
|
||||
} else {
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-file-alt w-6 h-6 mr-2 text-blue-500"></i>
|
||||
File Metadata
|
||||
</h1>
|
||||
File Metadata
|
||||
}
|
||||
</h1>
|
||||
<a href="/files/search" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
@@ -425,6 +423,26 @@ templ FileMetadataList(ctx context.Context, data FileMetadataListData) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Set dark background color if in dark mode
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
document.getElementById('file-metadata-container').style.backgroundColor = '#111827';
|
||||
}
|
||||
|
||||
// Add event listener for theme changes
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
setTimeout(function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
document.getElementById('file-metadata-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -432,213 +450,239 @@ templ FileMetadataList(ctx context.Context, data FileMetadataListData) {
|
||||
// FileMetadataDetails renders detailed information about a file
|
||||
templ FileMetadataDetails(ctx context.Context, data FileMetadataDetailsData) {
|
||||
@LayoutWithContext("File Details", ctx) {
|
||||
<div class="px-2 py-4 sm:px-4 w-full">
|
||||
<div class="mb-6">
|
||||
<a href="/files" class="inline-flex items-center text-blue-600 hover:underline dark:text-blue-500">
|
||||
<i class="fas fa-arrow-left mr-2"></i> Back to Files
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full">
|
||||
<!-- Card header -->
|
||||
<div class="p-4 md:p-5 border-b border-gray-200 dark:border-gray-700">
|
||||
<h5 class="text-xl font-bold leading-none text-gray-900 dark:text-white">
|
||||
{ data.File.FileName }
|
||||
</h5>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
File ID: { strconv.FormatUint(uint64(data.File.ID), 10) }
|
||||
</p>
|
||||
<div id="file-details-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||
<div class="pb-8 w-full">
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-file-alt w-6 h-6 mr-2 text-blue-500"></i>
|
||||
File Details: { data.File.FileName }
|
||||
</h1>
|
||||
<a href="/files" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-arrow-left mr-2"></i> Back to Files
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Card content -->
|
||||
<div class="p-4 md:p-5">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- File Information -->
|
||||
<div>
|
||||
<h6 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-file-alt mr-2 text-gray-500 dark:text-gray-400"></i> File Information
|
||||
</h6>
|
||||
<div class="overflow-x-auto relative shadow-md sm:rounded-lg">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<tbody>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Filename
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
{ data.File.FileName }
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Size
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
{ formatFileSize(data.File.FileSize) }
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Hash
|
||||
</th>
|
||||
<td class="py-3 px-4 break-all bg-white dark:bg-gray-800">
|
||||
if data.File.FileHash != "" {
|
||||
<span class="font-mono">{ data.File.FileHash }</span>
|
||||
} else {
|
||||
<span class="text-gray-400 dark:text-gray-500 italic">Not available</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Status
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
<span class={ "text-xs font-medium px-2.5 py-0.5 rounded", getStatusBadgeClass(data.File.Status) }>
|
||||
{ data.File.Status }
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Original Path
|
||||
</th>
|
||||
<td class="py-3 px-4 break-all bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-folder mr-2 text-yellow-500"></i>
|
||||
<span>{ data.File.OriginalPath }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Destination Path
|
||||
</th>
|
||||
<td class="py-3 px-4 break-all bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-folder-open mr-2 text-blue-500"></i>
|
||||
<span>{ data.File.DestinationPath }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Processing Information -->
|
||||
<div>
|
||||
<h6 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-cogs mr-2 text-gray-500 dark:text-gray-400"></i> Processing Information
|
||||
</h6>
|
||||
<div class="overflow-x-auto relative shadow-md sm:rounded-lg">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<tbody>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Job
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("/files/job/%d", data.File.JobID)) } class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
|
||||
{ data.File.Job.Name }
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Processed Time
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<i class="far fa-clock mr-2 text-gray-500"></i>
|
||||
<span>{ data.File.ProcessedTime.Format("2006-01-02 15:04:05") }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Creation Time
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-calendar-plus mr-2 text-green-500"></i>
|
||||
<span>{ data.File.CreationTime.Format("2006-01-02 15:04:05") }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Modification Time
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-calendar-alt mr-2 text-purple-500"></i>
|
||||
<span>{ data.File.ModTime.Format("2006-01-02 15:04:05") }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
if data.File.Status == "error" && data.File.ErrorMessage != "" {
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full">
|
||||
<!-- Card header -->
|
||||
<div class="p-4 md:p-5 border-b border-gray-200 dark:border-gray-700">
|
||||
<h5 class="text-xl font-bold leading-none text-gray-900 dark:text-white">
|
||||
{ data.File.FileName }
|
||||
</h5>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
File ID: { strconv.FormatUint(uint64(data.File.ID), 10) }
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card content -->
|
||||
<div class="p-4 md:p-5">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- File Information -->
|
||||
<div>
|
||||
<h6 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-file-alt mr-2 text-gray-500 dark:text-gray-400"></i> File Information
|
||||
</h6>
|
||||
<div class="overflow-x-auto relative shadow-md sm:rounded-lg">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<tbody>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Error
|
||||
Filename
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
{ data.File.FileName }
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Size
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
{ formatFileSize(data.File.FileSize) }
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Hash
|
||||
</th>
|
||||
<td class="py-3 px-4 break-all bg-white dark:bg-gray-800">
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-exclamation-triangle mt-1 mr-2 text-red-500"></i>
|
||||
<span class="text-red-600 dark:text-red-400">{ data.File.ErrorMessage }</span>
|
||||
if data.File.FileHash != "" {
|
||||
<span class="font-mono">{ data.File.FileHash }</span>
|
||||
} else {
|
||||
<span class="text-gray-400 dark:text-gray-500 italic">Not available</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Status
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
<span class={ "text-xs font-medium px-2.5 py-0.5 rounded", getStatusBadgeClass(data.File.Status) }>
|
||||
{ data.File.Status }
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Original Path
|
||||
</th>
|
||||
<td class="py-3 px-4 break-all bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-folder mr-2 text-yellow-500"></i>
|
||||
<span>{ data.File.OriginalPath }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Record Created
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
{ data.File.CreatedAt.Format("2006-01-02 15:04:05") }
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Record Updated
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
{ data.File.UpdatedAt.Format("2006-01-02 15:04:05") }
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Destination Path
|
||||
</th>
|
||||
<td class="py-3 px-4 break-all bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-folder-open mr-2 text-blue-500"></i>
|
||||
<span>{ data.File.DestinationPath }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Processing Information -->
|
||||
<div>
|
||||
<h6 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-cogs mr-2 text-gray-500 dark:text-gray-400"></i> Processing Information
|
||||
</h6>
|
||||
<div class="overflow-x-auto relative shadow-md sm:rounded-lg">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<tbody>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Job
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("/files/job/%d", data.File.JobID)) } class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
|
||||
{ data.File.Job.Name }
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Processed Time
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<i class="far fa-clock mr-2 text-gray-500"></i>
|
||||
<span>{ data.File.ProcessedTime.Format("2006-01-02 15:04:05") }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Creation Time
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-calendar-plus mr-2 text-green-500"></i>
|
||||
<span>{ data.File.CreationTime.Format("2006-01-02 15:04:05") }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Modification Time
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-calendar-alt mr-2 text-purple-500"></i>
|
||||
<span>{ data.File.ModTime.Format("2006-01-02 15:04:05") }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
if data.File.Status == "error" && data.File.ErrorMessage != "" {
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Error
|
||||
</th>
|
||||
<td class="py-3 px-4 break-all bg-white dark:bg-gray-800">
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-exclamation-triangle mt-1 mr-2 text-red-500"></i>
|
||||
<span class="text-red-600 dark:text-red-400">{ data.File.ErrorMessage }</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Record Created
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
{ data.File.CreatedAt.Format("2006-01-02 15:04:05") }
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="py-3 px-4 font-medium text-gray-900 whitespace-nowrap dark:text-white bg-gray-50 dark:bg-gray-800">
|
||||
Record Updated
|
||||
</th>
|
||||
<td class="py-3 px-4 bg-white dark:bg-gray-800">
|
||||
{ data.File.UpdatedAt.Format("2006-01-02 15:04:05") }
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete dialog -->
|
||||
@FileMetadataDialog(
|
||||
fmt.Sprintf("delete-file-dialog-%d", data.File.ID),
|
||||
"Delete File Metadata",
|
||||
fmt.Sprintf("Are you sure you want to delete the metadata for '%s'? This cannot be undone.", data.File.FileName),
|
||||
"btn-danger",
|
||||
"Delete",
|
||||
"delete",
|
||||
data.File.ID,
|
||||
data.File.FileName,
|
||||
"details",
|
||||
)
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="mt-6 flex flex-wrap justify-end gap-3">
|
||||
<a href="/files" class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
|
||||
<i class="fas fa-list mr-2"></i> Back to Files
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onclick={ showFileDialog(fmt.Sprintf("delete-file-dialog-%d", data.File.ID)) }
|
||||
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800">
|
||||
<i class="fas fa-trash mr-2"></i> Delete Record
|
||||
</button>
|
||||
|
||||
<!-- Delete dialog -->
|
||||
@FileMetadataDialog(
|
||||
fmt.Sprintf("delete-file-dialog-%d", data.File.ID),
|
||||
"Delete File Metadata",
|
||||
fmt.Sprintf("Are you sure you want to delete the metadata for '%s'? This cannot be undone.", data.File.FileName),
|
||||
"btn-danger",
|
||||
"Delete",
|
||||
"delete",
|
||||
data.File.ID,
|
||||
data.File.FileName,
|
||||
"details",
|
||||
)
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="mt-6 flex flex-wrap justify-end gap-3">
|
||||
<a href="/files" class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
|
||||
<i class="fas fa-list mr-2"></i> Back to Files
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onclick={ showFileDialog(fmt.Sprintf("delete-file-dialog-%d", data.File.ID)) }
|
||||
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800">
|
||||
<i class="fas fa-trash mr-2"></i> Delete Record
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Set dark background color if in dark mode
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
document.getElementById('file-details-container').style.backgroundColor = '#111827';
|
||||
}
|
||||
|
||||
// Add event listener for theme changes
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
setTimeout(function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
document.getElementById('file-details-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -646,82 +690,108 @@ templ FileMetadataDetails(ctx context.Context, data FileMetadataDetailsData) {
|
||||
// FileMetadataSearch renders an advanced search form for file metadata
|
||||
templ FileMetadataSearch(ctx context.Context, data FileMetadataSearchData) {
|
||||
@LayoutWithContext("Search Files", ctx) {
|
||||
<div class="px-2 py-4 sm:px-4 w-full">
|
||||
<div class="mb-6">
|
||||
<a href="/files" class="inline-flex items-center text-blue-600 hover:underline dark:text-blue-500">
|
||||
<i class="fas fa-arrow-left mr-2"></i> Back to Files
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full">
|
||||
<h5 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Advanced File Search</h5>
|
||||
<form
|
||||
hx-get="/files/search/partial"
|
||||
hx-target="#search-results-container"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#search-form-loading"
|
||||
hx-headers='{"X-HX-Request": "true"}'
|
||||
hx-boost="false"
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="job_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Job ID</label>
|
||||
<input type="text" id="job_id" name="job_id" value={ data.Filter.JobID } placeholder="Job ID"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="status" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Status</label>
|
||||
<select id="status" name="status" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="processed" selected?={ data.Filter.Status == "processed" }>Processed</option>
|
||||
<option value="archived" selected?={ data.Filter.Status == "archived" }>Archived</option>
|
||||
<option value="deleted" selected?={ data.Filter.Status == "deleted" }>Deleted</option>
|
||||
<option value="archived_and_deleted" selected?={ data.Filter.Status == "archived_and_deleted" }>Archived & Deleted</option>
|
||||
<option value="error" selected?={ data.Filter.Status == "error" }>Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filename" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Filename</label>
|
||||
<input type="text" id="filename" name="filename" value={ data.Filter.FileName } placeholder="Filename or partial match"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="hash" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">File Hash</label>
|
||||
<input type="text" id="hash" name="hash" value={ data.Filter.Hash } placeholder="MD5 hash"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="start_date" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Processed After</label>
|
||||
<input type="date" id="start_date" name="start_date" value={ data.Filter.StartDate }
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="end_date" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Processed Before</label>
|
||||
<input type="date" id="end_date" name="end_date" value={ data.Filter.EndDate }
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div class="md:col-span-2 flex justify-end">
|
||||
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-search mr-2"></i> Search Files
|
||||
</button>
|
||||
<div id="search-form-loading" class="htmx-indicator ml-2 flex items-center">
|
||||
<i class="fas fa-circle-notch fa-spin text-blue-600"></i>
|
||||
<div id="file-search-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||
<div class="pb-8 w-full">
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-search w-6 h-6 mr-2 text-blue-500"></i>
|
||||
File Metadata Search
|
||||
</h1>
|
||||
<a href="/files" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-arrow-left mr-2"></i> Back to Files
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full">
|
||||
<h5 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Advanced File Search</h5>
|
||||
<form
|
||||
hx-get="/files/search/partial"
|
||||
hx-target="#search-results-container"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#search-form-loading"
|
||||
hx-headers='{"X-HX-Request": "true"}'
|
||||
hx-boost="false"
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="job_id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Job ID</label>
|
||||
<input type="text" id="job_id" name="job_id" value={ data.Filter.JobID } placeholder="Job ID"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="status" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Status</label>
|
||||
<select id="status" name="status" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="processed" selected?={ data.Filter.Status == "processed" }>Processed</option>
|
||||
<option value="archived" selected?={ data.Filter.Status == "archived" }>Archived</option>
|
||||
<option value="deleted" selected?={ data.Filter.Status == "deleted" }>Deleted</option>
|
||||
<option value="archived_and_deleted" selected?={ data.Filter.Status == "archived_and_deleted" }>Archived & Deleted</option>
|
||||
<option value="error" selected?={ data.Filter.Status == "error" }>Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filename" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Filename</label>
|
||||
<input type="text" id="filename" name="filename" value={ data.Filter.FileName } placeholder="Filename or partial match"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="hash" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">File Hash</label>
|
||||
<input type="text" id="hash" name="hash" value={ data.Filter.Hash } placeholder="MD5 hash"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="start_date" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Processed After</label>
|
||||
<input type="date" id="start_date" name="start_date" value={ data.Filter.StartDate }
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="end_date" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Processed Before</label>
|
||||
<input type="date" id="end_date" name="end_date" value={ data.Filter.EndDate }
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div class="md:col-span-2 flex justify-end">
|
||||
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
<i class="fas fa-search mr-2"></i> Search Files
|
||||
</button>
|
||||
<div id="search-form-loading" class="htmx-indicator ml-2 flex items-center">
|
||||
<i class="fas fa-circle-notch fa-spin text-blue-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full">
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h5 class="text-lg font-semibold text-gray-900 dark:text-white">Search Results ({ strconv.FormatInt(data.TotalCount, 10) })</h5>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Page { strconv.Itoa(data.Page) } of { strconv.Itoa(data.TotalPages) }
|
||||
</span>
|
||||
</div>
|
||||
<div id="search-results-container" class="w-full">
|
||||
@FileMetadataSearchContent(data)
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 w-full">
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<h5 class="text-lg font-semibold text-gray-900 dark:text-white">Search Results ({ strconv.FormatInt(data.TotalCount, 10) })</h5>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Page { strconv.Itoa(data.Page) } of { strconv.Itoa(data.TotalPages) }
|
||||
</span>
|
||||
</div>
|
||||
<div id="search-results-container" class="w-full">
|
||||
@FileMetadataSearchContent(data)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Set dark background color if in dark mode
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
document.getElementById('file-search-container').style.backgroundColor = '#111827';
|
||||
}
|
||||
|
||||
// Add event listener for theme changes
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
setTimeout(function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
document.getElementById('file-search-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
+100
-78
@@ -264,91 +264,113 @@ templ HistoryContent(ctx context.Context, data HistoryData) {
|
||||
|
||||
templ History(ctx context.Context, data HistoryData) {
|
||||
@LayoutWithContext("Transfer History", ctx) {
|
||||
<div class="py-6 px-4 mx-auto max-w-7xl lg:px-8">
|
||||
<!-- Page Header -->
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-history w-6 h-6 mr-2 text-blue-500"></i>
|
||||
Transfer History
|
||||
</h1>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 rounded-lg px-4 py-2 inline-flex items-center">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<span class="font-medium">Total: { fmt.Sprint(data.Total) } transfers</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Pagination Controls -->
|
||||
<div class="mb-6 p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||
<form hx-get="/history" hx-target="#history-content" hx-indicator="#search-indicator" class="flex flex-col sm:flex-row gap-4 items-start sm:items-center">
|
||||
<div class="w-full sm:w-auto relative flex-grow">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i class="fas fa-search text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
value={ data.SearchTerm }
|
||||
placeholder="Search by job name or status..."
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 pr-3 py-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
/>
|
||||
<input type="hidden" name="page" value="1" />
|
||||
<input type="hidden" name="pageSize" value={ fmt.Sprint(data.PageSize) } />
|
||||
<div id="history-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||
<div class="pb-8 w-full">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-history w-6 h-6 mr-2 text-blue-500"></i>
|
||||
Transfer History
|
||||
</h1>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 rounded-lg px-4 py-2 inline-flex items-center">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<span class="font-medium">Total: { fmt.Sprint(data.Total) } transfers</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center py-2.5 px-4 text-sm font-medium text-white bg-blue-700 rounded-lg border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||
>
|
||||
<i class="fas fa-search mr-2 -ml-1"></i>
|
||||
Search
|
||||
<span id="search-indicator" class="htmx-indicator ml-2">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</span>
|
||||
</button>
|
||||
if data.SearchTerm != "" {
|
||||
</div>
|
||||
|
||||
<!-- Search and Pagination Controls -->
|
||||
<div class="mb-6 p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
||||
<form hx-get="/history" hx-target="#history-content" hx-indicator="#search-indicator" class="flex flex-col sm:flex-row gap-4 items-start sm:items-center">
|
||||
<div class="w-full sm:w-auto relative flex-grow">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i class="fas fa-search text-gray-400 dark:text-gray-500"></i>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
value={ data.SearchTerm }
|
||||
placeholder="Search by job name or status..."
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 pr-3 py-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
/>
|
||||
<input type="hidden" name="page" value="1" />
|
||||
<input type="hidden" name="pageSize" value={ fmt.Sprint(data.PageSize) } />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
hx-get="/history"
|
||||
hx-target="#history-content"
|
||||
hx-indicator="#clear-indicator"
|
||||
hx-vals={ fmt.Sprintf(`{"pageSize": %d}`, data.PageSize) }
|
||||
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 inline-flex items-center"
|
||||
type="submit"
|
||||
class="inline-flex items-center py-2.5 px-4 text-sm font-medium text-white bg-blue-700 rounded-lg border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||
>
|
||||
<i class="fas fa-times mr-2"></i>
|
||||
Clear
|
||||
<span id="clear-indicator" class="htmx-indicator ml-2">
|
||||
<i class="fas fa-search mr-2 -ml-1"></i>
|
||||
Search
|
||||
<span id="search-indicator" class="htmx-indicator ml-2">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<label class="inline-flex items-center text-sm text-gray-700 dark:text-gray-300">
|
||||
Show
|
||||
<select
|
||||
name="pageSize"
|
||||
hx-get="/history"
|
||||
hx-target="#history-content"
|
||||
hx-include="[name='search']"
|
||||
hx-indicator="#size-indicator"
|
||||
class="mx-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
>
|
||||
<option value="10" selected?={ data.PageSize == 10 }>10</option>
|
||||
<option value="25" selected?={ data.PageSize == 25 }>25</option>
|
||||
<option value="50" selected?={ data.PageSize == 50 }>50</option>
|
||||
<option value="100" selected?={ data.PageSize == 100 }>100</option>
|
||||
</select>
|
||||
entries
|
||||
<span id="size-indicator" class="htmx-indicator ml-2">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="history-content" class="mt-6">
|
||||
@HistoryContent(ctx, data)
|
||||
if data.SearchTerm != "" {
|
||||
<button
|
||||
hx-get="/history"
|
||||
hx-target="#history-content"
|
||||
hx-indicator="#clear-indicator"
|
||||
hx-vals={ fmt.Sprintf(`{"pageSize": %d}`, data.PageSize) }
|
||||
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 inline-flex items-center"
|
||||
>
|
||||
<i class="fas fa-times mr-2"></i>
|
||||
Clear
|
||||
<span id="clear-indicator" class="htmx-indicator ml-2">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<label class="inline-flex items-center text-sm text-gray-700 dark:text-gray-300">
|
||||
Show
|
||||
<select
|
||||
name="pageSize"
|
||||
hx-get="/history"
|
||||
hx-target="#history-content"
|
||||
hx-include="[name='search']"
|
||||
hx-indicator="#size-indicator"
|
||||
class="mx-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
>
|
||||
<option value="10" selected?={ data.PageSize == 10 }>10</option>
|
||||
<option value="25" selected?={ data.PageSize == 25 }>25</option>
|
||||
<option value="50" selected?={ data.PageSize == 50 }>50</option>
|
||||
<option value="100" selected?={ data.PageSize == 100 }>100</option>
|
||||
</select>
|
||||
entries
|
||||
<span id="size-indicator" class="htmx-indicator ml-2">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="history-content" class="mt-6">
|
||||
@HistoryContent(ctx, data)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Set dark background color if in dark mode
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
document.getElementById('history-container').style.backgroundColor = '#111827';
|
||||
}
|
||||
|
||||
// Add event listener for theme changes
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', function() {
|
||||
setTimeout(function() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
document.getElementById('history-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,9 +139,9 @@ templ NotificationDropdown(data NotificationsData) {
|
||||
<div class={ "w-11 h-11 rounded-full flex items-center justify-center", GetNotificationBgColor(notification.Type) }>
|
||||
<i class={ GetNotificationIcon(notification.Type) }></i>
|
||||
</div>
|
||||
<div class={ "flex absolute justify-center items-center ml-6 -mt-5 w-5 h-5 rounded-full border border-white dark:border-gray-700", GetNotificationBadgeColor(notification.Type) }>
|
||||
<i class={ "text-white text-xs", GetNotificationBadgeIcon(notification.Type) }></i>
|
||||
</div>
|
||||
// <div class={ "flex absolute justify-center items-center ml-6 -mt-5 w-5 h-5 rounded-full border border-white dark:border-gray-700", GetNotificationBadgeColor(notification.Type) }>
|
||||
// <i class={ "text-white text-xs", GetNotificationBadgeIcon(notification.Type) }></i>
|
||||
// </div>
|
||||
</div>
|
||||
<div class="pl-3 w-full">
|
||||
<div class="text-gray-500 font-normal text-sm mb-1.5 dark:text-gray-400">
|
||||
|
||||
+15
-13
@@ -28,9 +28,10 @@ type SettingsData struct {
|
||||
|
||||
templ Settings(ctx context.Context, data SettingsData) {
|
||||
@LayoutWithContext("Application Settings", ctx) {
|
||||
<div class="p-4 md:p-6 2xl:p-10">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<div id="settings-container" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||
<div class="pb-8 w-full">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<i class="fas fa-cog w-6 h-6 mr-2 text-blue-500"></i>
|
||||
Application Settings
|
||||
</h1>
|
||||
@@ -67,16 +68,16 @@ templ Settings(ctx context.Context, data SettingsData) {
|
||||
<i class="fas fa-bell mr-2"></i>Notifications
|
||||
</button>
|
||||
</li>
|
||||
<li class="mr-2" role="presentation">
|
||||
<button class="inline-block p-4 border-b-2 border-transparent rounded-t-lg hover:border-gray-300 hover:text-gray-600 dark:hover:text-gray-300" id="general-tab" data-tabs-target="#general" type="button" role="tab" aria-controls="general" aria-selected="false">
|
||||
<i class="fas fa-sliders-h mr-2"></i>General
|
||||
</button>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<button class="inline-block p-4 border-b-2 border-transparent rounded-t-lg hover:border-gray-300 hover:text-gray-600 dark:hover:text-gray-300" id="security-tab" data-tabs-target="#security" type="button" role="tab" aria-controls="security" aria-selected="false">
|
||||
<i class="fas fa-shield-alt mr-2"></i>Security
|
||||
</button>
|
||||
</li>
|
||||
// <li class="mr-2" role="presentation">
|
||||
// <button class="inline-block p-4 border-b-2 border-transparent rounded-t-lg hover:border-gray-300 hover:text-gray-600 dark:hover:text-gray-300" id="general-tab" data-tabs-target="#general" type="button" role="tab" aria-controls="general" aria-selected="false">
|
||||
// <i class="fas fa-sliders-h mr-2"></i>General
|
||||
// </button>
|
||||
// </li>
|
||||
// <li role="presentation">
|
||||
// <button class="inline-block p-4 border-b-2 border-transparent rounded-t-lg hover:border-gray-300 hover:text-gray-600 dark:hover:text-gray-300" id="security-tab" data-tabs-target="#security" type="button" role="tab" aria-controls="security" aria-selected="false">
|
||||
// <i class="fas fa-shield-alt mr-2"></i>Security
|
||||
// </button>
|
||||
// </li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -435,6 +436,7 @@ templ Settings(ctx context.Context, data SettingsData) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@toggleNotificationFields()
|
||||
}
|
||||
|
||||
@@ -7,17 +7,26 @@ import (
|
||||
)
|
||||
|
||||
type UserManagementData struct {
|
||||
Users []db.User
|
||||
Roles []db.Role
|
||||
ActiveTab string // "list", "create", or "edit"
|
||||
EditUser *db.User
|
||||
UserRoles []db.Role
|
||||
ErrorMessage string
|
||||
Users []db.User
|
||||
Roles []db.Role
|
||||
ActiveTab string // "list", "create", or "edit"
|
||||
EditUser *db.User
|
||||
UserRoles []db.Role
|
||||
ErrorMessage string
|
||||
SuccessMessage string
|
||||
}
|
||||
|
||||
templ UserManagement(ctx context.Context, data UserManagementData) {
|
||||
@LayoutWithContext("User Management", ctx) {
|
||||
<div class="p-4 md:p-6 2xl:p-10">
|
||||
@UserManagementContent(data)
|
||||
}
|
||||
}
|
||||
|
||||
// UserManagementContent is a partial component for HTMX to replace only the content
|
||||
templ UserManagementContent(data UserManagementData) {
|
||||
// <div id="userManagementContainer" class="px-4 py-8 mx-auto max-w-screen-xl lg:px-6">
|
||||
<div id="userManagementContainer" style="min-height: 100vh;" class="bg-gray-50 dark:bg-gray-900">
|
||||
<div class="pb-8 w-full">
|
||||
<!-- Header -->
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
@@ -25,38 +34,33 @@ templ UserManagement(ctx context.Context, data UserManagementData) {
|
||||
User Management
|
||||
</h1>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
<button
|
||||
class="flex items-center justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
hx-get="/admin/users/new"
|
||||
hx-target="#user-management-content"
|
||||
hx-get="/admin/users/new"
|
||||
hx-target="#userManagementContainer"
|
||||
hx-push-url="true"
|
||||
>
|
||||
<i class="fas fa-user-plus w-4 h-4 mr-2"></i>
|
||||
Add User
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center justify-center text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-300 dark:focus:ring-gray-700 border border-gray-200 dark:border-gray-600"
|
||||
hx-get="/admin/users"
|
||||
hx-target="#user-management-content"
|
||||
hx-push-url="true"
|
||||
>
|
||||
<i class="fas fa-list w-4 h-4 mr-2"></i>
|
||||
View All Users
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status and Error Messages -->
|
||||
<div id="status-message" class="hidden mb-4 p-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800" role="alert"></div>
|
||||
|
||||
if data.SuccessMessage != "" {
|
||||
<div id="status-message" class="mb-4 p-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800" role="alert">
|
||||
<div class="font-medium">{ data.SuccessMessage }</div>
|
||||
</div>
|
||||
} else {
|
||||
<div id="status-message" class="hidden mb-4 p-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800" role="alert"></div>
|
||||
}
|
||||
if data.ErrorMessage != "" {
|
||||
<div id="error-message" class="mb-4 p-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert">
|
||||
<div class="font-medium">{ data.ErrorMessage }</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Content -->
|
||||
<div id="user-management-content">
|
||||
<!-- Main Content Container -->
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 dark:bg-gray-800">
|
||||
<!-- Include the appropriate tab -->
|
||||
if data.ActiveTab == "list" || data.ActiveTab == "" {
|
||||
@userList(data.Users)
|
||||
} else if data.ActiveTab == "create" {
|
||||
@@ -65,7 +69,6 @@ templ UserManagement(ctx context.Context, data UserManagementData) {
|
||||
@userForm(data.EditUser, false, data.Roles)
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Help Notice -->
|
||||
<div class="mt-8">
|
||||
<div class="flex p-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900 dark:text-blue-400" role="alert">
|
||||
@@ -77,7 +80,7 @@ templ UserManagement(ctx context.Context, data UserManagementData) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
// User listing component
|
||||
@@ -149,17 +152,16 @@ templ userList(users []db.User) {
|
||||
<button
|
||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center me-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||
hx-get={ "/admin/users/" + strconv.Itoa(int(user.ID)) + "/edit" }
|
||||
hx-target="#user-management-content"
|
||||
hx-target="#userManagementContainer"
|
||||
hx-push-url="true"
|
||||
>
|
||||
<i class="fas fa-edit w-3.5 h-3.5 mr-1.5"></i> Edit
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800"
|
||||
hx-delete={ "/admin/users/" + strconv.Itoa(int(user.ID)) }
|
||||
hx-confirm="Are you sure you want to delete this user? This action cannot be undone."
|
||||
hx-target="#user-management-content"
|
||||
hx-target="#userManagementContainer"
|
||||
>
|
||||
<i class="fas fa-trash-alt w-3.5 h-3.5 mr-1.5"></i> Delete
|
||||
</button>
|
||||
@@ -193,15 +195,14 @@ templ userForm(user *db.User, isNew bool, availableRoles []db.Role) {
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="space-y-6"
|
||||
<form
|
||||
class="space-y-4 md:space-y-6"
|
||||
if isNew {
|
||||
hx-post="/admin/users"
|
||||
} else {
|
||||
hx-put={ "/admin/users/" + strconv.Itoa(int(user.ID)) }
|
||||
}
|
||||
hx-target="#user-management-content"
|
||||
hx-target="#userManagementContainer"
|
||||
x-data="{
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
@@ -214,19 +215,18 @@ templ userForm(user *db.User, isNew bool, availableRoles []db.Role) {
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
required
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
required
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="user@example.com"
|
||||
if !isNew {
|
||||
value={ user.Email }
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
if isNew {
|
||||
@@ -235,28 +235,27 @@ templ userForm(user *db.User, isNew bool, availableRoles []db.Role) {
|
||||
New Password (leave blank to keep current)
|
||||
}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
x-model="password"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="••••••••"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="••••••••"
|
||||
if isNew {
|
||||
required="true"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password_confirm" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm Password</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password_confirm"
|
||||
id="password_confirm"
|
||||
<input
|
||||
type="password"
|
||||
name="password_confirm"
|
||||
id="password_confirm"
|
||||
x-model="confirmPassword"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="••••••••"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="••••••••"
|
||||
if isNew {
|
||||
required="true"
|
||||
}
|
||||
@@ -265,15 +264,14 @@ templ userForm(user *db.User, isNew bool, availableRoles []db.Role) {
|
||||
Passwords do not match
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="pt-2">
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<input
|
||||
id="is_admin"
|
||||
name="is_admin"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800"
|
||||
<input
|
||||
id="is_admin"
|
||||
name="is_admin"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800"
|
||||
if !isNew && user.GetIsAdmin() {
|
||||
checked="true"
|
||||
}
|
||||
@@ -285,18 +283,16 @@ templ userForm(user *db.User, isNew bool, availableRoles []db.Role) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
if !isNew {
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
|
||||
<h4 class="text-base font-medium text-gray-900 dark:text-white mb-2">Account Status</h4>
|
||||
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<input
|
||||
id="account_locked"
|
||||
name="account_locked"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800"
|
||||
<input
|
||||
id="account_locked"
|
||||
name="account_locked"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800"
|
||||
if user.GetAccountLocked() {
|
||||
checked="true"
|
||||
}
|
||||
@@ -309,7 +305,6 @@ templ userForm(user *db.User, isNew bool, availableRoles []db.Role) {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
if len(availableRoles) > 0 {
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
|
||||
<h4 class="text-base font-medium text-gray-900 dark:text-white mb-2">Roles</h4>
|
||||
@@ -317,11 +312,11 @@ templ userForm(user *db.User, isNew bool, availableRoles []db.Role) {
|
||||
for _, role := range availableRoles {
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<input
|
||||
<input
|
||||
id={ "role_" + strconv.Itoa(int(role.ID)) }
|
||||
name="roles[]"
|
||||
name="roles[]"
|
||||
value={ strconv.Itoa(int(role.ID)) }
|
||||
type="checkbox"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800"
|
||||
if !isNew && hasRole(user, role.ID) {
|
||||
checked="true"
|
||||
@@ -338,20 +333,19 @@ templ userForm(user *db.User, isNew bool, availableRoles []db.Role) {
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
|
||||
hx-get="/admin/users"
|
||||
hx-target="#user-management-content"
|
||||
hx-target="#userManagementContainer"
|
||||
hx-push-url="true"
|
||||
>
|
||||
<i class="fas fa-arrow-left mr-2"></i>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
<button
|
||||
type="submit"
|
||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||
x-bind:disabled="!passwordsMatch() && confirmPassword !== ''"
|
||||
>
|
||||
@@ -376,4 +370,4 @@ func hasRole(user *db.User, roleID uint) bool {
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user