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.
This commit is contained in:
StarFleetCPTN
2025-03-20 20:39:26 -07:00
parent 73362793ad
commit 9a49d87777
4 changed files with 438 additions and 57 deletions
+197 -10
View File
@@ -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 = '<div class="text-center py-4 text-secondary-500 dark:text-secondary-400">No configurations selected</div>';
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 = `
<div class="flex items-center">
<span class="inline-flex items-center justify-center h-6 w-6 rounded-full bg-primary-100 dark:bg-primary-900 mr-2 text-primary-700 dark:text-primary-300 text-sm">${index + 1}</span>
<span class="font-medium text-secondary-700 dark:text-secondary-300">${configName}</span>
</div>
<div class="flex space-x-1">
<button type="button" class="move-up p-1 rounded hover:bg-secondary-100 dark:hover:bg-secondary-700" title="Move up">
<i class="fas fa-arrow-up text-secondary-500"></i>
</button>
<button type="button" class="move-down p-1 rounded hover:bg-secondary-100 dark:hover:bg-secondary-700" title="Move down">
<i class="fas fa-arrow-down text-secondary-500"></i>
</button>
</div>
`;
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);
});
</script>
}
@@ -95,6 +261,7 @@ templ JobForm(ctx context.Context, data JobFormData) {
if data.IsNew {
<form
id="new-job-form"
class="space-y-6"
hx-post="/jobs"
hx-target="body"
@@ -161,10 +328,19 @@ templ JobForm(ctx context.Context, data JobFormData) {
</div>
</div>
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
<i class="fas fa-info-circle mr-1"></i>
Select one or more configurations to run on this schedule.
</p>
<!-- Selected Configurations Order List -->
<div class="mt-4">
<label class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
<i class="fas fa-sort-amount-down mr-1"></i> Execution Order
</label>
<div id="selected-configs" class="border border-secondary-300 dark:border-secondary-700 rounded-md p-3 min-h-20 bg-secondary-50 dark:bg-secondary-900">
<!-- Selected items will be populated by JavaScript -->
</div>
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
<i class="fas fa-info-circle mr-1"></i>
Use the arrows to change the order in which configurations will execute.
</p>
</div>
</div>
<div>
@@ -341,10 +517,12 @@ templ JobForm(ctx context.Context, data JobFormData) {
</form>
} else {
<form
id="edit-job-form"
class="space-y-6"
hx-post={ fmt.Sprintf("/jobs/%d", data.Job.ID) }
hx-target="body"
hx-boost="true">
hx-boost="true"
data-config-order={ data.Job.ConfigIDs }>
<div class="space-y-6">
<div>
<label for="name" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Job Name</label>
@@ -411,10 +589,19 @@ templ JobForm(ctx context.Context, data JobFormData) {
</div>
</div>
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
<i class="fas fa-info-circle mr-1"></i>
Select one or more configurations to run on this schedule.
</p>
<!-- Selected Configurations Order List for edit -->
<div class="mt-4">
<label class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
<i class="fas fa-sort-amount-down mr-1"></i> Execution Order
</label>
<div id="selected-configs-edit" class="border border-secondary-300 dark:border-secondary-700 rounded-md p-3 min-h-20 bg-secondary-50 dark:bg-secondary-900">
<!-- Selected items will be populated by JavaScript -->
</div>
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
<i class="fas fa-info-circle mr-1"></i>
Use the arrows to change the order in which configurations will execute.
</p>
</div>
</div>
<div>
@@ -599,7 +786,7 @@ templ JobForm(ctx context.Context, data JobFormData) {
<div class="px-8 py-4 bg-secondary-50 dark:bg-secondary-800 border-t border-secondary-200 dark:border-secondary-700 text-center">
<p class="text-sm text-secondary-600 dark:text-secondary-400">
<i class="fas fa-info-circle mr-1"></i>
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
</p>
</div>
</div>
+37 -2
View File
@@ -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
+36 -5
View File
@@ -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
+168 -40
View File
@@ -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())