From 9a49d87777365474d72167c3b582baa174732b8b Mon Sep 17 00:00:00 2001 From: StarFleetCPTN Date: Thu, 20 Mar 2025 20:39:26 -0700 Subject: [PATCH] Implement Job Configuration Ordering in Job Form - Added functionality to allow users to select and reorder job configurations in both new and edit job forms. - Introduced JavaScript logic to handle the display and ordering of selected configurations, including move up/down buttons. - Updated backend to process and store the order of configurations when creating or updating jobs. - Enhanced logging for job creation and update processes to include configuration order details. --- components/job_form.templ | 207 +++++++++++++++++++++++-- internal/db/db.go | 39 ++++- internal/scheduler/scheduler.go | 41 ++++- internal/web/handlers/job_handlers.go | 208 +++++++++++++++++++++----- 4 files changed, 438 insertions(+), 57 deletions(-) diff --git a/components/job_form.templ b/components/job_form.templ index 9e09ee3..9875c5e 100644 --- a/components/job_form.templ +++ b/components/job_form.templ @@ -72,6 +72,172 @@ templ configSearchScript() { }); }); } + + // Handle job ordering + const setupJobOrdering = (configListId, selectedListId, formId, savedOrder) => { + const configList = document.getElementById(configListId); + const selectedList = document.getElementById(selectedListId); + const form = document.getElementById(formId); + + if (!configList || !selectedList || !form) return; + + // Get saved order if available + const orderedIds = savedOrder ? savedOrder.split(',').map(id => id.trim()) : []; + console.log('Initial saved order:', orderedIds); + + // Initialize selected items from checked checkboxes + const updateSelectedItems = (initialLoad = false) => { + // Clear current list + selectedList.innerHTML = ''; + + // Get all checked checkboxes + const checkedItems = configList.querySelectorAll('input[type="checkbox"]:checked'); + + if (checkedItems.length === 0) { + selectedList.innerHTML = '
No configurations selected
'; + return; + } + + // Create a map of config items for easy access + const configItems = {}; + checkedItems.forEach(checkbox => { + configItems[checkbox.value] = { + checkbox: checkbox, + configId: checkbox.value, + configName: checkbox.nextElementSibling.textContent.trim() + }; + }); + + // If we have a saved order and this is the initial load, use that order + let itemsToShow = []; + if (initialLoad && orderedIds.length > 0) { + // First add items in the saved order + orderedIds.forEach(id => { + if (configItems[id]) { + itemsToShow.push(configItems[id]); + delete configItems[id]; // Remove from map to avoid duplicates + } + }); + + // Then add any remaining checked items not in the saved order + Object.values(configItems).forEach(item => { + itemsToShow.push(item); + }); + } else { + // Just add all checked items in their current order + itemsToShow = Object.values(configItems); + } + + // Add each item to the selected list + itemsToShow.forEach((item, index) => { + const configId = item.configId; + const configName = item.configName; + + const listItem = document.createElement('div'); + listItem.className = 'flex items-center justify-between p-2 mb-2 bg-white dark:bg-secondary-800 border border-secondary-200 dark:border-secondary-700 rounded-lg'; + listItem.setAttribute('data-id', configId); + + listItem.innerHTML = ` +
+ ${index + 1} + ${configName} +
+
+ + +
+ `; + + selectedList.appendChild(listItem); + }); + + // Update hidden order inputs + updateOrderInputs(); + }; + + // Update hidden inputs with the current order + const updateOrderInputs = () => { + const items = selectedList.querySelectorAll('.flex.items-center.justify-between'); + if (items.length === 0) return; + + // Remove any existing order input to avoid duplicates + const existingOrderInput = form.querySelector('input[name="config_order"]'); + if (existingOrderInput) { + existingOrderInput.remove(); + } + + // Create a new input with the current order + const orderedIds = Array.from(items).map(item => item.getAttribute('data-id')); + + // Create a hidden input to store the order + const configOrderInput = document.createElement('input'); + configOrderInput.type = 'hidden'; + configOrderInput.name = 'config_order'; + configOrderInput.value = orderedIds.join(','); + + // Add the input to the form + form.appendChild(configOrderInput); + + // Update the visible order numbers + items.forEach((item, index) => { + const orderNum = index + 1; + const orderSpan = item.querySelector('span.rounded-full'); + if (orderSpan) { + orderSpan.textContent = orderNum; + } + }); + + console.log('Updated order input:', configOrderInput.value); + }; + + // Initialize the selected list with saved order if available + updateSelectedItems(true); + + // Handle checkbox changes + configList.addEventListener('change', (e) => { + if (e.target.matches('input[type="checkbox"]')) { + updateSelectedItems(false); + } + }); + + // Handle reordering + selectedList.addEventListener('click', (e) => { + const listItem = e.target.closest('.flex.items-center.justify-between'); + if (!listItem) return; + + if (e.target.closest('.move-up')) { + const prev = listItem.previousElementSibling; + if (prev) { + selectedList.insertBefore(listItem, prev); + updateOrderInputs(); + } + } else if (e.target.closest('.move-down')) { + const next = listItem.nextElementSibling; + if (next) { + selectedList.insertBefore(next, listItem); + updateOrderInputs(); + } + } + }); + + // Ensure the order input is updated before submission + form.addEventListener('submit', function(e) { + updateOrderInputs(); + console.log('Form submitted with order:', form.querySelector('input[name="config_order"]')?.value); + }); + }; + + // Setup ordering for new job form + setupJobOrdering('config-list', 'selected-configs', 'new-job-form', null); + + // Setup ordering for edit job form + const editJobForm = document.getElementById('edit-job-form'); + const savedOrderEdit = editJobForm ? editJobForm.getAttribute('data-config-order') : null; + setupJobOrdering('config-list-edit', 'selected-configs-edit', 'edit-job-form', savedOrderEdit); }); } @@ -95,6 +261,7 @@ templ JobForm(ctx context.Context, data JobFormData) { if data.IsNew {
-

- - Select one or more configurations to run on this schedule. -

+ +
+ +
+ +
+

+ + Use the arrows to change the order in which configurations will execute. +

+
@@ -341,10 +517,12 @@ templ JobForm(ctx context.Context, data JobFormData) { } else {
+ hx-boost="true" + data-config-order={ data.Job.ConfigIDs }>
@@ -411,10 +589,19 @@ templ JobForm(ctx context.Context, data JobFormData) {
-

- - Select one or more configurations to run on this schedule. -

+ +
+ +
+ +
+

+ + Use the arrows to change the order in which configurations will execute. +

+
@@ -599,7 +786,7 @@ templ JobForm(ctx context.Context, data JobFormData) {

- Jobs will run according to their schedule and execute the selected transfer configuration + Jobs will run according to their schedule and execute the selected transfer configurations in the order specified

diff --git a/internal/db/db.go b/internal/db/db.go index a8d9397..8678a91 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -2,6 +2,7 @@ package db import ( "fmt" + "log" "os" "os/exec" "path/filepath" @@ -186,6 +187,9 @@ func (j *Job) SetConfigIDsList(ids []uint) { // Join with commas j.ConfigIDs = strings.Join(strIDs, ",") + // Debug log the final ConfigIDs string + log.Printf("SetConfigIDsList: Setting ConfigIDs to: %s (from %v)", j.ConfigIDs, ids) + // If there's at least one ID, set ConfigID to the first one for backward compatibility if len(ids) > 0 { j.ConfigID = ids[0] @@ -376,8 +380,25 @@ func (db *DB) GetJob(id uint) (*Job, error) { } func (db *DB) UpdateJob(job *Job) error { + log.Printf("UpdateJob: Updating job ID: %d, ConfigIDs: %s", job.ID, job.ConfigIDs) + // Use Omit to prevent GORM from updating or creating a new config - return db.Omit("Config").Save(job).Error + return db.Model(&Job{}). + Where("id = ?", job.ID). + Omit("Config"). + Updates(map[string]interface{}{ + "name": job.Name, + "config_id": job.ConfigID, + "config_ids": job.ConfigIDs, // Explicitly update config_ids + "schedule": job.Schedule, + "enabled": job.Enabled, + "webhook_enabled": job.WebhookEnabled, + "webhook_url": job.WebhookURL, + "webhook_secret": job.WebhookSecret, + "webhook_headers": job.WebhookHeaders, + "notify_on_success": job.NotifyOnSuccess, + "notify_on_failure": job.NotifyOnFailure, + }).Error } func (db *DB) DeleteJob(id uint) error { @@ -965,7 +986,21 @@ func (db *DB) GetConfigsForJob(jobID uint) ([]TransferConfig, error) { return nil, err } - return configs, nil + // Create a map for quick lookup + configMap := make(map[uint]TransferConfig) + for _, config := range configs { + configMap[config.ID] = config + } + + // Create a new slice with configs in the correct order + orderedConfigs := make([]TransferConfig, 0, len(configs)) + for _, configID := range configIDs { + if config, exists := configMap[configID]; exists { + orderedConfigs = append(orderedConfigs, config) + } + } + + return orderedConfigs, nil } // GetSkipProcessedFiles returns the value of SkipProcessedFiles with a default if nil diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 0ecc809..2a06228 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -347,14 +347,45 @@ func (s *Scheduler) executeJob(jobID uint) { return } - s.log.LogDebug("Processing %d configurations: %+v", len(configs), configs) + s.log.LogDebug("Loaded %d configurations for job %d", len(configs), jobID) if len(configs) == 0 { s.log.LogError("Error: job %d has no associated configurations", jobID) return } - s.log.LogInfo("Loaded job %d with %d configurations", jobID, len(configs)) + // Get the ordered config IDs from the job + orderedConfigIDs := job.GetConfigIDsList() + s.log.LogDebug("Ordered config IDs for job %d: %v", jobID, orderedConfigIDs) + + // Create a map of configs for easy lookup + configMap := make(map[uint]db.TransferConfig) + for _, config := range configs { + configMap[config.ID] = config + } + + // Process configurations in the specified order + var orderedConfigs []db.TransferConfig + + // First, add configs in the order specified in the job's ConfigIDs + for _, configID := range orderedConfigIDs { + if config, exists := configMap[configID]; exists { + orderedConfigs = append(orderedConfigs, config) + delete(configMap, configID) // Remove from map to avoid duplicates + } + } + + // Add any remaining configs not in the ordered list (shouldn't happen, but just in case) + for _, config := range configMap { + orderedConfigs = append(orderedConfigs, config) + } + + s.log.LogInfo("Processing job %d with %d configurations in specified order", jobID, len(orderedConfigs)) + + // Log the order of execution + for i, config := range orderedConfigs { + s.log.LogDebug("Execution order %d/%d: Config ID %d (%s)", i+1, len(orderedConfigs), config.ID, config.Name) + } // Update job last run time startTime := time.Now() @@ -363,9 +394,9 @@ func (s *Scheduler) executeJob(jobID uint) { s.log.LogError("Error updating job last run time for job %d: %v", jobID, err) } - // Process each configuration - for i, config := range configs { - s.processConfiguration(&job, &config, i+1, len(configs)) + // Process each configuration in the specified order + for i, config := range orderedConfigs { + s.processConfiguration(&job, &config, i+1, len(orderedConfigs)) } // Update next run time after execution diff --git a/internal/web/handlers/job_handlers.go b/internal/web/handlers/job_handlers.go index 14e6aa8..47739fd 100644 --- a/internal/web/handlers/job_handlers.go +++ b/internal/web/handlers/job_handlers.go @@ -2,8 +2,10 @@ package handlers import ( "fmt" + "log" "net/http" "strconv" + "strings" "github.com/gin-gonic/gin" "github.com/starfleetcptn/gomft/components" @@ -141,6 +143,9 @@ func (h *Handlers) HandleEditJob(c *gin.Context) { func (h *Handlers) HandleCreateJob(c *gin.Context) { userID := c.GetUint("userID") + // Debug logging + log.Printf("HandleCreateJob: Form data received: %v", c.Request.PostForm) + // Parse form data var job db.Job if err := c.ShouldBind(&job); err != nil { @@ -148,6 +153,9 @@ func (h *Handlers) HandleCreateJob(c *gin.Context) { return } + // Debug logging + log.Printf("HandleCreateJob: Job after binding: %+v", job) + // Get multiple config IDs from form configIDs := c.PostFormArray("config_ids[]") if len(configIDs) == 0 { @@ -155,35 +163,83 @@ func (h *Handlers) HandleCreateJob(c *gin.Context) { return } + // Debug logging + log.Printf("HandleCreateJob: config_ids[]: %v", configIDs) + // Process config IDs var configIDsList []uint - for _, configIDStr := range configIDs { - configID, err := strconv.ParseUint(configIDStr, 10, 32) - if err != nil { - c.String(http.StatusBadRequest, "Invalid configuration ID format") - return - } - // Verify that the config exists and belongs to the user - var config db.TransferConfig - if err := h.DB.First(&config, configID).Error; err != nil { - c.String(http.StatusBadRequest, "Invalid configuration selected") - return - } + // Check if we have an explicit order specified + configOrder := c.PostForm("config_order") + log.Printf("HandleCreateJob: config_order: %s", configOrder) - // Check if the config belongs to the user - if config.CreatedBy != userID { - // Check if user is admin - isAdmin, exists := c.Get("isAdmin") - if !exists || isAdmin != true { - c.String(http.StatusForbidden, "You do not have permission to use this configuration") + if configOrder != "" { + // Parse the ordered list + orderStrings := strings.Split(configOrder, ",") + log.Printf("HandleCreateJob: order strings: %v", orderStrings) + + for _, configIDStr := range orderStrings { + configID, err := strconv.ParseUint(configIDStr, 10, 32) + if err != nil { + log.Printf("HandleCreateJob: Error parsing config ID: %v", err) + c.String(http.StatusBadRequest, "Invalid configuration ID format in order") return } - } - configIDsList = append(configIDsList, uint(configID)) + // Verify that the config exists and belongs to the user + var config db.TransferConfig + if err := h.DB.First(&config, configID).Error; err != nil { + log.Printf("HandleCreateJob: Invalid config ID: %d, error: %v", configID, err) + c.String(http.StatusBadRequest, "Invalid configuration selected") + return + } + + // Check if the config belongs to the user + if config.CreatedBy != userID { + // Check if user is admin + isAdmin, exists := c.Get("isAdmin") + if !exists || isAdmin != true { + c.String(http.StatusForbidden, "You do not have permission to use this configuration") + return + } + } + + configIDsList = append(configIDsList, uint(configID)) + } + } else { + // Fall back to unordered config IDs + log.Printf("HandleCreateJob: No config_order found, using checkbox order") + for _, configIDStr := range configIDs { + configID, err := strconv.ParseUint(configIDStr, 10, 32) + if err != nil { + c.String(http.StatusBadRequest, "Invalid configuration ID format") + return + } + + // Verify that the config exists and belongs to the user + var config db.TransferConfig + if err := h.DB.First(&config, configID).Error; err != nil { + c.String(http.StatusBadRequest, "Invalid configuration selected") + return + } + + // Check if the config belongs to the user + if config.CreatedBy != userID { + // Check if user is admin + isAdmin, exists := c.Get("isAdmin") + if !exists || isAdmin != true { + c.String(http.StatusForbidden, "You do not have permission to use this configuration") + return + } + } + + configIDsList = append(configIDsList, uint(configID)) + } } + // Debug logging + log.Printf("HandleCreateJob: Final configIDsList: %v", configIDsList) + // Set the first config ID for backward compatibility if len(configIDsList) > 0 { job.ConfigID = configIDsList[0] @@ -214,6 +270,10 @@ func (h *Handlers) HandleCreateJob(c *gin.Context) { // Set the config IDs list job.SetConfigIDsList(configIDsList) + // Debug logging + log.Printf("HandleCreateJob: Job after setting ConfigIDsList: %+v", job) + log.Printf("HandleCreateJob: Job.ConfigIDs: %s", job.ConfigIDs) + // Set the boolean fields - handle both "on" and "true" values for checkboxes enabledVal := c.Request.FormValue("enabled") jobEnabledValue := enabledVal == "on" || enabledVal == "true" @@ -239,10 +299,13 @@ func (h *Handlers) HandleCreateJob(c *gin.Context) { // Create the job if err := h.DB.CreateJob(&job); err != nil { + log.Printf("HandleCreateJob: Error creating job: %v", err) c.String(http.StatusInternalServerError, "Failed to create job") return } + log.Printf("HandleCreateJob: Job successfully created with ID: %d", job.ID) + // Schedule the job with the scheduler if err := h.Scheduler.ScheduleJob(&job); err != nil { c.String(http.StatusInternalServerError, "Job created but scheduling failed: "+err.Error()) @@ -257,8 +320,13 @@ func (h *Handlers) HandleUpdateJob(c *gin.Context) { id := c.Param("id") userID := c.GetUint("userID") + // Debug logging + log.Printf("HandleUpdateJob: Updating job ID: %s", id) + log.Printf("HandleUpdateJob: Form data received: %v", c.Request.PostForm) + var job db.Job if err := h.DB.First(&job, id).Error; err != nil { + log.Printf("HandleUpdateJob: Job not found: %v", err) c.String(http.StatusNotFound, "Job not found") return } @@ -275,49 +343,102 @@ func (h *Handlers) HandleUpdateJob(c *gin.Context) { // Get the old job values for comparison oldJob := job + log.Printf("HandleUpdateJob: Original job: %+v", oldJob) + log.Printf("HandleUpdateJob: Original job ConfigIDs: %s", oldJob.ConfigIDs) // Parse form data if err := c.ShouldBind(&job); err != nil { + log.Printf("HandleUpdateJob: Error binding form data: %v", err) c.String(http.StatusBadRequest, "Invalid form data") return } + log.Printf("HandleUpdateJob: Job after binding: %+v", job) + // Get multiple config IDs from form configIDs := c.PostFormArray("config_ids[]") if len(configIDs) == 0 { + log.Printf("HandleUpdateJob: No config_ids[] found in form data") c.String(http.StatusBadRequest, "At least one configuration must be selected") return } + log.Printf("HandleUpdateJob: config_ids[]: %v", configIDs) + // Process config IDs var configIDsList []uint - for _, configIDStr := range configIDs { - configID, err := strconv.ParseUint(configIDStr, 10, 32) - if err != nil { - c.String(http.StatusBadRequest, "Invalid configuration ID format") - return - } - // Verify that the config exists - var config db.TransferConfig - if err := h.DB.First(&config, configID).Error; err != nil { - c.String(http.StatusBadRequest, "Invalid configuration selected") - return - } + // Check if we have an explicit order specified + configOrder := c.PostForm("config_order") + log.Printf("HandleUpdateJob: config_order: %s", configOrder) - // Check if the config belongs to the user - if config.CreatedBy != userID { - // Check if user is admin - isAdmin, exists := c.Get("isAdmin") - if !exists || isAdmin != true { - c.String(http.StatusForbidden, "You do not have permission to use this configuration") + if configOrder != "" { + // Parse the ordered list + orderStrings := strings.Split(configOrder, ",") + log.Printf("HandleUpdateJob: order strings: %v", orderStrings) + + for _, configIDStr := range orderStrings { + configID, err := strconv.ParseUint(configIDStr, 10, 32) + if err != nil { + log.Printf("HandleUpdateJob: Error parsing config ID: %v", err) + c.String(http.StatusBadRequest, "Invalid configuration ID format in order") return } - } - configIDsList = append(configIDsList, uint(configID)) + // Verify that the config exists + var config db.TransferConfig + if err := h.DB.First(&config, configID).Error; err != nil { + log.Printf("HandleUpdateJob: Invalid config ID: %d, error: %v", configID, err) + c.String(http.StatusBadRequest, "Invalid configuration selected") + return + } + + // Check if the config belongs to the user + if config.CreatedBy != userID { + // Check if user is admin + isAdmin, exists := c.Get("isAdmin") + if !exists || isAdmin != true { + c.String(http.StatusForbidden, "You do not have permission to use this configuration") + return + } + } + + configIDsList = append(configIDsList, uint(configID)) + } + } else { + // Fall back to unordered config IDs + log.Printf("HandleUpdateJob: No config_order found, using checkbox order") + for _, configIDStr := range configIDs { + configID, err := strconv.ParseUint(configIDStr, 10, 32) + if err != nil { + c.String(http.StatusBadRequest, "Invalid configuration ID format") + return + } + + // Verify that the config exists + var config db.TransferConfig + if err := h.DB.First(&config, configID).Error; err != nil { + c.String(http.StatusBadRequest, "Invalid configuration selected") + return + } + + // Check if the config belongs to the user + if config.CreatedBy != userID { + // Check if user is admin + isAdmin, exists := c.Get("isAdmin") + if !exists || isAdmin != true { + c.String(http.StatusForbidden, "You do not have permission to use this configuration") + return + } + } + + configIDsList = append(configIDsList, uint(configID)) + } } + // Debug logging + log.Printf("HandleUpdateJob: Final configIDsList: %v", configIDsList) + // Set the first config ID for backward compatibility if len(configIDsList) > 0 { job.ConfigID = configIDsList[0] @@ -332,6 +453,10 @@ func (h *Handlers) HandleUpdateJob(c *gin.Context) { // Set the config IDs list job.SetConfigIDsList(configIDsList) + // Debug logging + log.Printf("HandleUpdateJob: Job after setting ConfigIDsList: %+v", job) + log.Printf("HandleUpdateJob: Job.ConfigIDs: %s", job.ConfigIDs) + // Set the boolean fields - handle both "on" and "true" values for checkboxes enabledVal := c.Request.FormValue("enabled") jobEnabledValue := enabledVal == "on" || enabledVal == "true" @@ -357,10 +482,13 @@ func (h *Handlers) HandleUpdateJob(c *gin.Context) { job.Config = db.TransferConfig{} if err := h.DB.UpdateJob(&job); err != nil { + log.Printf("HandleUpdateJob: Error updating job: %v", err) c.String(http.StatusInternalServerError, "Failed to update job") return } + log.Printf("HandleUpdateJob: Job successfully updated") + // Reschedule the job with the scheduler if err := h.Scheduler.ScheduleJob(&job); err != nil { c.String(http.StatusInternalServerError, "Job updated but scheduling failed: "+err.Error())