mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 15:41:20 +02:00
- Add support for multiple transfer configurations per job, allowing users to select one or more configurations. - Update job creation and editing forms to handle multiple configuration selections with checkboxes. - Enhance job processing logic to iterate through all associated configurations during execution. - Introduce database migrations to add necessary fields for storing multiple configuration IDs. - Update dashboard and history views to display configuration details for jobs. - Refactor related templates and handlers to accommodate the new multi-configuration functionality.
370 lines
17 KiB
Templ
370 lines
17 KiB
Templ
package components
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"github.com/starfleetcptn/gomft/internal/db"
|
|
)
|
|
|
|
type HistoryData struct {
|
|
History []db.JobHistory
|
|
CurrentPage int
|
|
TotalPages int
|
|
SearchTerm string
|
|
PageSize int
|
|
Total int
|
|
Configs map[uint]db.TransferConfig // Map of config IDs to configs for quick lookup
|
|
}
|
|
|
|
// min returns the smaller of x or y
|
|
func min(x, y int) int {
|
|
if x < y {
|
|
return x
|
|
}
|
|
return y
|
|
}
|
|
|
|
// getConfigNameForHistory returns the appropriate name for the config used in a job history entry
|
|
func getConfigNameForHistory(history db.JobHistory, configs map[uint]db.TransferConfig) string {
|
|
// If ConfigID is set in the history record, use that to get the config name
|
|
if history.ConfigID > 0 {
|
|
if config, exists := configs[history.ConfigID]; exists {
|
|
return config.Name
|
|
}
|
|
}
|
|
|
|
// Fallback to the Job's default Config if it exists
|
|
if history.Job.Config.ID > 0 {
|
|
return history.Job.Config.Name
|
|
}
|
|
|
|
// If we can't determine the config name, show a default with the job name
|
|
if history.Job.Name != "" {
|
|
return fmt.Sprintf("%s (unknown config)", history.Job.Name)
|
|
}
|
|
|
|
return "Unknown Configuration"
|
|
}
|
|
|
|
// HistoryContent renders only the content part of the history page for HTMX requests
|
|
templ HistoryContent(ctx context.Context, data HistoryData) {
|
|
if len(data.History) == 0 {
|
|
<div class="text-center py-12 bg-white dark:bg-secondary-800 rounded-lg shadow">
|
|
<div class="inline-block p-4 rounded-full bg-secondary-100 dark:bg-secondary-700 mb-4">
|
|
<i class="fas fa-inbox text-secondary-400 dark:text-secondary-500 text-3xl"></i>
|
|
</div>
|
|
<h3 class="mt-2 text-lg font-medium text-secondary-900 dark:text-secondary-100">No transfer history</h3>
|
|
if data.SearchTerm != "" {
|
|
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">No results found for "{ data.SearchTerm }". Try a different search term or <button hx-get="/history" hx-target="#history-content" hx-vals={ fmt.Sprintf(`{"pageSize": %d}`, data.PageSize) } class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300">clear search</button>.</p>
|
|
} else if data.CurrentPage > 1 {
|
|
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">No more results on this page. <button hx-get="/history" hx-target="#history-content" hx-vals={ fmt.Sprintf(`{"page": 1, "pageSize": %d}`, data.PageSize) } class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300">Return to first page</button>.</p>
|
|
} else {
|
|
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">Transfer history will appear here once jobs have run.</p>
|
|
}
|
|
</div>
|
|
} else {
|
|
<div class="bg-white dark:bg-secondary-800 shadow overflow-hidden rounded-lg">
|
|
<ul role="list" class="divide-y divide-secondary-200 dark:divide-secondary-700">
|
|
for _, history := range data.History {
|
|
<li class="hover:bg-secondary-50 dark:hover:bg-secondary-750 transition-colors">
|
|
<div class="px-4 py-4 sm:px-6">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<p class="text-sm font-medium text-primary-600 dark:text-primary-400 truncate">{ getConfigNameForHistory(history, data.Configs) }</p>
|
|
if history.Status == "completed" {
|
|
<span class="ml-2 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-300">
|
|
Completed
|
|
</span>
|
|
} else if history.Status == "failed" {
|
|
<span class="ml-2 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-300">
|
|
Failed
|
|
</span>
|
|
} else {
|
|
<span class="ml-2 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-300">
|
|
{ history.Status }
|
|
</span>
|
|
}
|
|
</div>
|
|
<div>
|
|
<a href={ templ.SafeURL(fmt.Sprintf("/job-runs/%d", history.ID)) }
|
|
class="inline-flex items-center px-3 py-1.5 border border-secondary-300 dark:border-secondary-600 text-sm leading-5 font-medium rounded-full text-secondary-700 dark:text-secondary-200 bg-white dark:bg-secondary-700 hover:bg-secondary-50 dark:hover:bg-secondary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-colors">
|
|
<i class="fas fa-eye mr-1"></i>
|
|
View Details
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div class="mt-2 sm:flex sm:justify-between">
|
|
<div class="sm:flex">
|
|
<p class="flex items-center text-sm text-secondary-500 dark:text-secondary-400">
|
|
<i class="fas fa-calendar-alt flex-shrink-0 mr-1.5 h-5 w-5 text-secondary-400 dark:text-secondary-500"></i>
|
|
Started: { history.StartTime.Format("Jan 02, 2006 15:04:05") }
|
|
</p>
|
|
if history.EndTime != nil {
|
|
<p class="mt-2 flex items-center text-sm text-secondary-500 dark:text-secondary-400 sm:mt-0 sm:ml-6">
|
|
<i class="fas fa-clock flex-shrink-0 mr-1.5 h-5 w-5 text-secondary-400 dark:text-secondary-500"></i>
|
|
Duration: { history.EndTime.Sub(history.StartTime).String() }
|
|
</p>
|
|
}
|
|
</div>
|
|
<div class="mt-2 flex items-center text-sm text-secondary-500 dark:text-secondary-400 sm:mt-0">
|
|
if history.BytesTransferred > 0 {
|
|
<i class="fas fa-upload flex-shrink-0 mr-1.5 h-5 w-5 text-secondary-400 dark:text-secondary-500"></i>
|
|
<p>
|
|
{ formatBytes(history.BytesTransferred) } transferred
|
|
</p>
|
|
}
|
|
</div>
|
|
</div>
|
|
if history.ErrorMessage != "" {
|
|
<div class="mt-2 p-2 bg-red-50 dark:bg-red-900/20 rounded border border-red-200 dark:border-red-800">
|
|
<p class="text-sm text-red-600 dark:text-red-400">
|
|
<i class="fas fa-exclamation-triangle mr-1"></i>
|
|
Error: { history.ErrorMessage }
|
|
</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
if data.TotalPages > 1 {
|
|
<div class="mt-6 flex flex-col sm:flex-row justify-between items-center py-4 bg-secondary-50 dark:bg-secondary-800 rounded-lg">
|
|
<div class="flex-1 flex justify-between gap-4 sm:hidden w-full px-4 mb-4 sm:mb-0">
|
|
if data.CurrentPage > 1 {
|
|
<button
|
|
hx-get="/history"
|
|
hx-target="#history-content"
|
|
hx-vals={ fmt.Sprintf(`{"page": %d, "pageSize": %d, "search": "%s"}`, data.CurrentPage-1, data.PageSize, data.SearchTerm) }
|
|
hx-indicator="#mobile-prev-indicator"
|
|
class="flex-1 h-10 flex items-center justify-center bg-secondary-200 text-secondary-800 hover:bg-secondary-300 rounded dark:bg-secondary-700 dark:text-secondary-200 dark:hover:bg-secondary-600 relative">
|
|
<span class="flex items-center">
|
|
<i class="fas fa-chevron-left mr-1"></i> Previous
|
|
</span>
|
|
<span id="mobile-prev-indicator" class="htmx-indicator absolute right-2">
|
|
<i class="fas fa-spinner fa-spin text-xs"></i>
|
|
</span>
|
|
</button>
|
|
} else {
|
|
<span class="flex-1 h-10 flex items-center justify-center bg-secondary-100 text-secondary-300 rounded dark:bg-secondary-800 dark:text-secondary-600 cursor-not-allowed">
|
|
<i class="fas fa-chevron-left mr-1"></i> Previous
|
|
</span>
|
|
}
|
|
|
|
if data.CurrentPage < data.TotalPages {
|
|
<button
|
|
hx-get="/history"
|
|
hx-target="#history-content"
|
|
hx-vals={ fmt.Sprintf(`{"page": %d, "pageSize": %d, "search": "%s"}`, data.CurrentPage+1, data.PageSize, data.SearchTerm) }
|
|
hx-indicator="#mobile-next-indicator"
|
|
class="flex-1 h-10 flex items-center justify-center bg-secondary-200 text-secondary-800 hover:bg-secondary-300 rounded dark:bg-secondary-700 dark:text-secondary-200 dark:hover:bg-secondary-600 relative">
|
|
<span class="flex items-center">
|
|
Next <i class="fas fa-chevron-right ml-1"></i>
|
|
</span>
|
|
<span id="mobile-next-indicator" class="htmx-indicator absolute right-2">
|
|
<i class="fas fa-spinner fa-spin text-xs"></i>
|
|
</span>
|
|
</button>
|
|
} else {
|
|
<span class="flex-1 h-10 flex items-center justify-center bg-secondary-100 text-secondary-300 rounded dark:bg-secondary-800 dark:text-secondary-600 cursor-not-allowed">
|
|
Next <i class="fas fa-chevron-right ml-1"></i>
|
|
</span>
|
|
}
|
|
</div>
|
|
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between px-4">
|
|
<div>
|
|
<p class="text-sm text-secondary-700 dark:text-secondary-300">
|
|
Showing
|
|
<span class="font-medium">{ fmt.Sprint((data.CurrentPage-1)*data.PageSize + 1) }</span>
|
|
to
|
|
<span class="font-medium">{ fmt.Sprint(min((data.CurrentPage)*data.PageSize, data.Total)) }</span>
|
|
of
|
|
<span class="font-medium">{ fmt.Sprint(data.Total) }</span>
|
|
results
|
|
</p>
|
|
</div>
|
|
<div class="flex justify-center">
|
|
<nav class="flex gap-2" aria-label="Pagination">
|
|
if data.CurrentPage > 1 {
|
|
<button
|
|
hx-get="/history"
|
|
hx-target="#history-content"
|
|
hx-vals={ fmt.Sprintf(`{"page": %d, "pageSize": %d, "search": "%s"}`, data.CurrentPage-1, data.PageSize, data.SearchTerm) }
|
|
hx-indicator="#prev-indicator"
|
|
class="w-10 h-10 flex items-center justify-center bg-secondary-200 text-secondary-800 hover:bg-secondary-300 rounded dark:bg-secondary-700 dark:text-secondary-200 dark:hover:bg-secondary-600 relative">
|
|
<i class="fas fa-chevron-left"></i>
|
|
<span id="prev-indicator" class="htmx-indicator absolute top-0 right-0 w-4 h-4 -mt-1 -mr-1">
|
|
<i class="fas fa-spinner fa-spin text-xs"></i>
|
|
</span>
|
|
</button>
|
|
} else {
|
|
<span class="w-10 h-10 flex items-center justify-center bg-secondary-100 text-secondary-300 rounded dark:bg-secondary-800 dark:text-secondary-600 cursor-not-allowed">
|
|
<i class="fas fa-chevron-left"></i>
|
|
</span>
|
|
}
|
|
|
|
<!-- Page numbers -->
|
|
@pageNumbers(data.CurrentPage, data.TotalPages, data.PageSize, data.SearchTerm)
|
|
|
|
if data.CurrentPage < data.TotalPages {
|
|
<button
|
|
hx-get="/history"
|
|
hx-target="#history-content"
|
|
hx-vals={ fmt.Sprintf(`{"page": %d, "pageSize": %d, "search": "%s"}`, data.CurrentPage+1, data.PageSize, data.SearchTerm) }
|
|
hx-indicator="#next-indicator"
|
|
class="w-10 h-10 flex items-center justify-center bg-secondary-200 text-secondary-800 hover:bg-secondary-300 rounded dark:bg-secondary-700 dark:text-secondary-200 dark:hover:bg-secondary-600 relative">
|
|
<i class="fas fa-chevron-right"></i>
|
|
<span id="next-indicator" class="htmx-indicator absolute top-0 right-0 w-4 h-4 -mt-1 -mr-1">
|
|
<i class="fas fa-spinner fa-spin text-xs"></i>
|
|
</span>
|
|
</button>
|
|
} else {
|
|
<span class="w-10 h-10 flex items-center justify-center bg-secondary-100 text-secondary-300 rounded dark:bg-secondary-800 dark:text-secondary-600 cursor-not-allowed">
|
|
<i class="fas fa-chevron-right"></i>
|
|
</span>
|
|
}
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
|
|
templ History(ctx context.Context, data HistoryData) {
|
|
@LayoutWithContext("Transfer History", ctx) {
|
|
<div id="history-page" class="py-6">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex items-center justify-between mb-8">
|
|
<h1 class="text-3xl font-bold text-secondary-900 dark:text-secondary-100">
|
|
<i class="fas fa-history mr-2 text-primary-600 dark:text-primary-400"></i>
|
|
Transfer History
|
|
</h1>
|
|
<div class="text-sm text-secondary-500 dark:text-secondary-400">
|
|
<span class="font-medium">Total: { fmt.Sprint(data.Total) } transfers</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search and Pagination Controls -->
|
|
<div class="mt-4 flex flex-col sm:flex-row justify-between items-center bg-secondary-50 dark:bg-secondary-800 p-4 rounded-lg mb-6">
|
|
<form hx-get="/history" hx-target="#history-content" hx-indicator="#search-indicator" class="w-full sm:w-auto mb-4 sm:mb-0">
|
|
<div class="flex">
|
|
<div class="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-secondary-400 dark:text-secondary-500"></i>
|
|
</div>
|
|
<input
|
|
type="text"
|
|
name="search"
|
|
value={ data.SearchTerm }
|
|
placeholder="Search by job name or status..."
|
|
class="block w-full pl-10 pr-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md leading-5 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-secondary-100 placeholder-secondary-500 dark:placeholder-secondary-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
|
/>
|
|
<input type="hidden" name="page" value="1" />
|
|
<input type="hidden" name="pageSize" value={ fmt.Sprint(data.PageSize) } />
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
class="ml-3 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:bg-primary-500 dark:hover:bg-primary-600 transition-colors"
|
|
>
|
|
<span>Search</span>
|
|
<span id="search-indicator" class="htmx-indicator ml-1">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
</span>
|
|
</button>
|
|
if data.SearchTerm != "" {
|
|
<button
|
|
hx-get="/history"
|
|
hx-target="#history-content"
|
|
hx-indicator="#clear-indicator"
|
|
hx-vals={ fmt.Sprintf(`{"pageSize": %d}`, data.PageSize) }
|
|
class="ml-2 inline-flex items-center px-4 py-2 border border-secondary-300 dark:border-secondary-600 text-sm font-medium rounded-md text-secondary-700 dark:text-secondary-200 bg-white dark:bg-secondary-700 hover:bg-secondary-50 dark:hover:bg-secondary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-colors"
|
|
>
|
|
<i class="fas fa-times mr-1"></i>
|
|
Clear
|
|
<span id="clear-indicator" class="htmx-indicator ml-1">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
</span>
|
|
</button>
|
|
}
|
|
</div>
|
|
</form>
|
|
|
|
<div class="flex items-center">
|
|
<span class="text-sm text-secondary-700 dark:text-secondary-300">
|
|
Show
|
|
<select
|
|
name="pageSize"
|
|
hx-get="/history"
|
|
hx-target="#history-content"
|
|
hx-include="[name='search']"
|
|
hx-indicator="#size-indicator"
|
|
class="mx-1 rounded-md border-secondary-300 dark:border-secondary-600 py-1 text-base bg-white dark:bg-secondary-700 text-secondary-900 dark:text-secondary-100 focus:border-primary-500 focus:outline-none focus:ring-primary-500 sm:text-sm"
|
|
>
|
|
<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-1">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="history-content" class="mt-6">
|
|
@HistoryContent(ctx, data)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
templ pageNumbers(currentPage int, totalPages int, pageSize int, searchTerm string) {
|
|
// Show at most 5 page numbers with the current page in the middle when possible
|
|
for i := 1; i <= totalPages; i++ {
|
|
if i == currentPage {
|
|
<span aria-current="page" class="w-10 h-10 flex items-center justify-center bg-primary-600 text-white rounded">
|
|
{ fmt.Sprint(i) }
|
|
</span>
|
|
} else if i == 1 || i == totalPages || (i >= currentPage-2 && i <= currentPage+2) {
|
|
<button
|
|
hx-get="/history"
|
|
hx-target="#history-content"
|
|
hx-vals={ fmt.Sprintf(`{"page": %d, "pageSize": %d, "search": "%s"}`, i, pageSize, searchTerm) }
|
|
hx-indicator={ fmt.Sprintf("#page-indicator-%d", i) }
|
|
class="w-10 h-10 flex items-center justify-center bg-secondary-200 text-secondary-800 hover:bg-secondary-300 rounded dark:bg-secondary-700 dark:text-secondary-200 dark:hover:bg-secondary-600 relative">
|
|
{ fmt.Sprint(i) }
|
|
<span id={ fmt.Sprintf("page-indicator-%d", i) } class="htmx-indicator absolute top-0 right-0 w-4 h-4 -mt-1 -mr-1">
|
|
<i class="fas fa-spinner fa-spin text-xs"></i>
|
|
</span>
|
|
</button>
|
|
} else if i == currentPage-3 || i == currentPage+3 {
|
|
<span class="w-10 h-10 flex items-center justify-center">...</span>
|
|
}
|
|
}
|
|
}
|
|
|
|
func formatBytes(bytes int64) string {
|
|
const unit = 1024
|
|
if bytes < unit {
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := bytes / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
}
|
|
|
|
func max(x, y int) int {
|
|
if x > y {
|
|
return x
|
|
}
|
|
return y
|
|
} |