diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go
index 346e717d9..5ca524dd2 100644
--- a/weed/admin/dash/admin_server.go
+++ b/weed/admin/dash/admin_server.go
@@ -10,6 +10,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/seaweedfs/seaweedfs/weed/admin/maintenance"
+ "github.com/seaweedfs/seaweedfs/weed/admin/plugin"
"github.com/seaweedfs/seaweedfs/weed/cluster"
"github.com/seaweedfs/seaweedfs/weed/credential"
"github.com/seaweedfs/seaweedfs/weed/glog"
@@ -108,6 +109,9 @@ type AdminServer struct {
// Worker gRPC server
workerGrpcServer *WorkerGrpcServer
+ // Plugin system manager
+ PluginManager *plugin.Manager
+
// Collection statistics caching
collectionStatsCache map[string]collectionStats
lastCollectionStatsUpdate time.Time
diff --git a/weed/admin/handlers/admin_handlers.go b/weed/admin/handlers/admin_handlers.go
index 8581d7162..8beb7ed74 100644
--- a/weed/admin/handlers/admin_handlers.go
+++ b/weed/admin/handlers/admin_handlers.go
@@ -26,6 +26,7 @@ type AdminHandlers struct {
maintenanceHandlers *MaintenanceHandlers
mqHandlers *MessageQueueHandlers
serviceAccountHandlers *ServiceAccountHandlers
+ pluginHandlers *PluginHandlers
}
// NewAdminHandlers creates a new instance of AdminHandlers
@@ -38,6 +39,7 @@ func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers {
maintenanceHandlers := NewMaintenanceHandlers(adminServer)
mqHandlers := NewMessageQueueHandlers(adminServer)
serviceAccountHandlers := NewServiceAccountHandlers(adminServer)
+ pluginHandlers := NewPluginHandlers(adminServer)
return &AdminHandlers{
adminServer: adminServer,
authHandlers: authHandlers,
@@ -48,6 +50,7 @@ func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers {
maintenanceHandlers: maintenanceHandlers,
mqHandlers: mqHandlers,
serviceAccountHandlers: serviceAccountHandlers,
+ pluginHandlers: pluginHandlers,
}
}
@@ -128,6 +131,11 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser,
protected.POST("/maintenance/config/:taskType", dash.RequireWriteAccess(), h.maintenanceHandlers.UpdateTaskConfig)
protected.GET("/maintenance/tasks/:id", h.maintenanceHandlers.ShowTaskDetail)
+ // Plugin system routes
+ protected.GET("/plugins", h.pluginHandlers.ShowPlugins)
+ protected.GET("/plugins/jobs/:jobType", h.pluginHandlers.ShowPluginJobs)
+ protected.GET("/plugins/config/:jobType", h.pluginHandlers.ShowPluginConfig)
+
// API routes for AJAX calls
api := r.Group("/api")
api.Use(dash.RequireAuthAPI()) // Use API-specific auth middleware
@@ -242,6 +250,19 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser,
maintenanceApi.PUT("/config", dash.RequireWriteAccess(), h.adminServer.UpdateMaintenanceConfigAPI)
}
+ // Plugin system API routes
+ pluginApi := api.Group("/plugin")
+ {
+ pluginApi.GET("/list", h.pluginHandlers.ListPluginsAPI)
+ pluginApi.GET("/jobs/:jobType", h.pluginHandlers.ListJobsAPI)
+ pluginApi.GET("/config/:jobType", h.pluginHandlers.GetConfigAPI)
+ pluginApi.POST("/config/:jobType/apply", dash.RequireWriteAccess(), h.pluginHandlers.SaveConfigAPI)
+ pluginApi.GET("/detection/history/:jobType", h.pluginHandlers.GetDetectionHistoryAPI)
+ pluginApi.GET("/execution/history/:jobType", h.pluginHandlers.GetExecutionHistoryAPI)
+ pluginApi.POST("/jobs/:jobType/trigger-detection", dash.RequireWriteAccess(), h.pluginHandlers.TriggerDetectionAPI)
+ pluginApi.POST("/jobs/:jobID/cancel", dash.RequireWriteAccess(), h.pluginHandlers.CancelJobAPI)
+ }
+
// Message Queue API routes
mqApi := api.Group("/mq")
{
diff --git a/weed/admin/handlers/plugin_handlers.go b/weed/admin/handlers/plugin_handlers.go
new file mode 100644
index 000000000..2bb1a6ec8
--- /dev/null
+++ b/weed/admin/handlers/plugin_handlers.go
@@ -0,0 +1,533 @@
+package handlers
+
+import (
+ "fmt"
+ "net/http"
+ "sort"
+ "strconv"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/seaweedfs/seaweedfs/weed/admin/dash"
+ "github.com/seaweedfs/seaweedfs/weed/admin/plugin"
+ "github.com/seaweedfs/seaweedfs/weed/admin/view/app"
+ "github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
+)
+
+// PluginHandlers handles HTTP requests for the plugin system
+type PluginHandlers struct {
+ adminServer *dash.AdminServer
+}
+
+// NewPluginHandlers creates a new plugin handlers instance
+func NewPluginHandlers(adminServer *dash.AdminServer) *PluginHandlers {
+ return &PluginHandlers{
+ adminServer: adminServer,
+ }
+}
+
+// ShowPlugins renders the main plugins page
+func (h *PluginHandlers) ShowPlugins(c *gin.Context) {
+ data := h.getPluginsData()
+ html := app.PluginsPage(data)
+ c.Header("Content-Type", "text/html")
+ html.Render(c.Request.Context(), c.Writer)
+}
+
+// ShowPluginJobs renders the job monitoring page
+func (h *PluginHandlers) ShowPluginJobs(c *gin.Context) {
+ jobType := c.Param("jobType")
+ data := h.getJobsData(jobType)
+ html := app.PluginJobsPage(data)
+ c.Header("Content-Type", "text/html")
+ html.Render(c.Request.Context(), c.Writer)
+}
+
+// ShowPluginConfig renders the plugin configuration page
+func (h *PluginHandlers) ShowPluginConfig(c *gin.Context) {
+ jobType := c.Param("jobType")
+ data := h.getPluginConfigData(jobType)
+ html := app.PluginConfigPage(data)
+ c.Header("Content-Type", "text/html")
+ html.Render(c.Request.Context(), c.Writer)
+}
+
+// ListPluginsAPI returns JSON list of registered plugins
+func (h *PluginHandlers) ListPluginsAPI(c *gin.Context) {
+ if h.adminServer.PluginManager == nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
+ return
+ }
+
+ plugins := h.adminServer.PluginManager.ListConnectedPlugins()
+
+ // Convert to API response
+ var pluginList []map[string]interface{}
+ for _, p := range plugins {
+ pluginList = append(pluginList, map[string]interface{}{
+ "id": p.ID,
+ "name": p.Name,
+ "version": p.Version,
+ "protocol_version": p.ProtocolVersion,
+ "connected_at": p.ConnectedAt,
+ "last_heartbeat": p.LastHeartbeat,
+ "state": p.State.String(),
+ "healthy": p.IsHealthy(),
+ "pending_jobs": p.PendingJobs,
+ "running_jobs": p.RunningJobs,
+ "cpu_usage": p.CPUUsagePercent,
+ "memory_usage_mb": p.MemoryUsageMB,
+ })
+ }
+
+ c.JSON(http.StatusOK, gin.H{"plugins": pluginList})
+}
+
+// ListJobsAPI returns JSON list of jobs for a job type
+func (h *PluginHandlers) ListJobsAPI(c *gin.Context) {
+ jobType := c.Param("jobType")
+ stateStr := c.DefaultQuery("state", "")
+ limit := 100
+
+ if h.adminServer.PluginManager == nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
+ return
+ }
+
+ var state plugin.JobState
+ if stateStr != "" {
+ // Parse state string
+ switch stateStr {
+ case "pending":
+ state = plugin.JobStatePending
+ case "running":
+ state = plugin.JobStateRunning
+ case "completed":
+ state = plugin.JobStateCompleted
+ case "failed":
+ state = plugin.JobStateFailed
+ default:
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid state"})
+ return
+ }
+ }
+
+ jobs := h.adminServer.PluginManager.ListJobs(jobType, state)
+ if limit > 0 && len(jobs) > limit {
+ jobs = jobs[:limit]
+ }
+
+ var jobList []map[string]interface{}
+ for _, j := range jobs {
+ jobList = append(jobList, map[string]interface{}{
+ "id": j.ID,
+ "type": j.Type,
+ "description": j.Description,
+ "state": j.State.String(),
+ "created_at": j.CreatedAt,
+ "updated_at": j.UpdatedAt,
+ "executor_id": j.ExecutorID,
+ "progress": j.ProgressPercent,
+ "retries": j.Retries,
+ })
+ }
+
+ c.JSON(http.StatusOK, gin.H{"jobs": jobList, "job_type": jobType})
+}
+
+// GetConfigAPI returns the configuration for a job type
+func (h *PluginHandlers) GetConfigAPI(c *gin.Context) {
+ jobType := c.Param("jobType")
+
+ if h.adminServer.PluginManager == nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
+ return
+ }
+
+ config, err := h.adminServer.PluginManager.GetConfig(jobType)
+ if err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("config not found: %v", err)})
+ return
+ }
+
+ adminConfigMap := convertConfigFieldsToMap(config.AdminConfig)
+ workerConfigMap := convertConfigFieldsToMap(config.WorkerConfig)
+
+ c.JSON(http.StatusOK, gin.H{
+ "job_type": jobType,
+ "enabled": config.Enabled,
+ "admin_config": adminConfigMap,
+ "worker_config": workerConfigMap,
+ "created_at": config.CreatedAt,
+ "updated_at": config.UpdatedAt,
+ "created_by": config.CreatedBy,
+ })
+}
+
+// SaveConfigAPI saves the configuration for a job type
+func (h *PluginHandlers) SaveConfigAPI(c *gin.Context) {
+ jobType := c.Param("jobType")
+
+ if h.adminServer.PluginManager == nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
+ return
+ }
+
+ var req struct {
+ Enabled bool `json:"enabled"`
+ AdminConfig map[string]interface{} `json:"admin_config"`
+ WorkerConfig map[string]interface{} `json:"worker_config"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid request: %v", err)})
+ return
+ }
+
+ // Convert maps back to config fields
+ adminConfigFields := convertMapToConfigFields(req.AdminConfig)
+ workerConfigFields := convertMapToConfigFields(req.WorkerConfig)
+
+ err := h.adminServer.PluginManager.SaveConfig(jobType, req.Enabled, adminConfigFields, workerConfigFields)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to save config: %v", err)})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"success": true, "message": "configuration saved"})
+}
+
+// GetDetectionHistoryAPI returns detection history for a job type
+func (h *PluginHandlers) GetDetectionHistoryAPI(c *gin.Context) {
+ jobType := c.Param("jobType")
+ limitStr := c.DefaultQuery("limit", "10")
+
+ if h.adminServer.PluginManager == nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
+ return
+ }
+
+ limit, err := strconv.Atoi(limitStr)
+ if err != nil || limit > 100 {
+ limit = 10
+ }
+
+ records := h.adminServer.PluginManager.GetDetectionHistory(jobType, limit)
+
+ var historyList []map[string]interface{}
+ for _, rec := range records {
+ historyList = append(historyList, map[string]interface{}{
+ "timestamp": rec.Timestamp,
+ "duration_ms": rec.DurationMs,
+ "jobs_detected": rec.JobsDetected,
+ "status": rec.Status,
+ "error": rec.Error,
+ "worker_id": rec.WorkerID,
+ "detailed_logs": rec.DetailedLogs,
+ })
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "job_type": jobType,
+ "history": historyList,
+ "count": len(historyList),
+ })
+}
+
+// GetExecutionHistoryAPI returns execution history for a job type
+func (h *PluginHandlers) GetExecutionHistoryAPI(c *gin.Context) {
+ jobType := c.Param("jobType")
+ limitStr := c.DefaultQuery("limit", "10")
+
+ if h.adminServer.PluginManager == nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
+ return
+ }
+
+ limit, err := strconv.Atoi(limitStr)
+ if err != nil || limit > 100 {
+ limit = 10
+ }
+
+ records := h.adminServer.PluginManager.GetExecutionHistory(jobType, limit)
+
+ var historyList []map[string]interface{}
+ for _, rec := range records {
+ historyList = append(historyList, map[string]interface{}{
+ "job_id": rec.JobID,
+ "job_type": rec.JobType,
+ "started_at": rec.StartedAt,
+ "completed_at": rec.CompletedAt,
+ "duration_ms": rec.DurationMs,
+ "success": rec.Success,
+ "error": rec.Error,
+ "worker_id": rec.WorkerID,
+ "items_processed": rec.ItemsProcessed,
+ "execution_logs": rec.ExecutionLogs,
+ })
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "job_type": jobType,
+ "history": historyList,
+ "count": len(historyList),
+ })
+}
+
+// TriggerDetectionAPI manually triggers detection for a job type
+func (h *PluginHandlers) TriggerDetectionAPI(c *gin.Context) {
+ jobType := c.Param("jobType")
+
+ if h.adminServer.PluginManager == nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
+ return
+ }
+
+ err := h.adminServer.PluginManager.TriggerDetection(jobType)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("detection failed: %v", err)})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"success": true, "message": "detection triggered"})
+}
+
+// CancelJobAPI cancels a running job
+func (h *PluginHandlers) CancelJobAPI(c *gin.Context) {
+ jobID := c.Param("jobID")
+
+ if h.adminServer.PluginManager == nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
+ return
+ }
+
+ err := h.adminServer.PluginManager.CancelJob(jobID)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("cancel failed: %v", err)})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"success": true, "message": "job cancelled"})
+}
+
+// getPluginsData prepares data for the main plugins page
+func (h *PluginHandlers) getPluginsData() map[string]interface{} {
+ data := map[string]interface{}{
+ "plugins": []map[string]interface{}{},
+ "job_types": []string{},
+ "total_plugins": 0,
+ "total_jobs": 0,
+ }
+
+ if h.adminServer.PluginManager == nil {
+ return data
+ }
+
+ // Get connected plugins
+ plugins := h.adminServer.PluginManager.ListConnectedPlugins()
+ var pluginList []map[string]interface{}
+ for _, p := range plugins {
+ pluginList = append(pluginList, map[string]interface{}{
+ "id": p.ID,
+ "name": p.Name,
+ "version": p.Version,
+ "connected_at": p.ConnectedAt.Format(time.RFC3339),
+ "state": p.State.String(),
+ "healthy": p.IsHealthy(),
+ "pending_jobs": p.PendingJobs,
+ "running_jobs": p.RunningJobs,
+ "cpu_usage": fmt.Sprintf("%.1f%%", p.CPUUsagePercent),
+ "memory_usage": fmt.Sprintf("%.1f MB", p.MemoryUsageMB),
+ })
+ }
+
+ // Get registered job types
+ jobTypes := h.adminServer.PluginManager.ListJobTypes()
+ sort.Strings(jobTypes) // Sort alphabetically
+
+ // Count jobs
+ totalJobs := 0
+ for _, jobType := range jobTypes {
+ jobs := h.adminServer.PluginManager.ListJobs(jobType, 0)
+ totalJobs += len(jobs)
+ }
+
+ data["plugins"] = pluginList
+ data["job_types"] = jobTypes
+ data["total_plugins"] = len(plugins)
+ data["total_jobs"] = totalJobs
+
+ return data
+}
+
+// getJobsData prepares data for the job monitoring page
+func (h *PluginHandlers) getJobsData(jobType string) map[string]interface{} {
+ data := map[string]interface{}{
+ "job_type": jobType,
+ "jobs": []map[string]interface{}{},
+ "pending_count": 0,
+ "running_count": 0,
+ "completed_count": 0,
+ "failed_count": 0,
+ }
+
+ if h.adminServer.PluginManager == nil {
+ return data
+ }
+
+ // Get all jobs for this type
+ allJobs := h.adminServer.PluginManager.ListJobs(jobType, 0)
+
+ var jobsList []map[string]interface{}
+ pendingCount, runningCount, completedCount, failedCount := 0, 0, 0, 0
+
+ for _, job := range allJobs {
+ jobsList = append(jobsList, map[string]interface{}{
+ "id": job.ID,
+ "description": job.Description,
+ "state": job.State.String(),
+ "created_at": job.CreatedAt.Format(time.RFC3339),
+ "updated_at": job.UpdatedAt.Format(time.RFC3339),
+ "progress": job.ProgressPercent,
+ "executor_id": job.ExecutorID,
+ "retries": job.Retries,
+ })
+
+ switch job.State {
+ case plugin.JobStatePending:
+ pendingCount++
+ case plugin.JobStateRunning:
+ runningCount++
+ case plugin.JobStateCompleted:
+ completedCount++
+ case plugin.JobStateFailed:
+ failedCount++
+ }
+ }
+
+ data["jobs"] = jobsList
+ data["pending_count"] = pendingCount
+ data["running_count"] = runningCount
+ data["completed_count"] = completedCount
+ data["failed_count"] = failedCount
+
+ return data
+}
+
+// getPluginConfigData prepares data for the plugin configuration page
+func (h *PluginHandlers) getPluginConfigData(jobType string) map[string]interface{} {
+ data := map[string]interface{}{
+ "job_type": jobType,
+ "enabled": false,
+ "admin_config": map[string]interface{}{},
+ "worker_config": map[string]interface{}{},
+ "schema": map[string]interface{}{},
+ "detection_history": []map[string]interface{}{},
+ "execution_history": []map[string]interface{}{},
+ }
+
+ if h.adminServer.PluginManager == nil {
+ return data
+ }
+
+ // Get config
+ config, err := h.adminServer.PluginManager.GetConfig(jobType)
+ if err == nil && config != nil {
+ data["enabled"] = config.Enabled
+ data["admin_config"] = convertConfigFieldsToMap(config.AdminConfig)
+ data["worker_config"] = convertConfigFieldsToMap(config.WorkerConfig)
+ }
+
+ // Get schema
+ // detector, err := h.adminServer.PluginManager.GetDetectorForJobType(jobType)
+ // if err == nil && detector != nil {
+ // data["schema"] = detector.ConfigSchema // Will be extended later
+ // }
+
+ // Get detection history
+ detectionRecords := h.adminServer.PluginManager.GetDetectionHistory(jobType, 10)
+ var detectionList []map[string]interface{}
+ for _, rec := range detectionRecords {
+ detectionList = append(detectionList, map[string]interface{}{
+ "timestamp": rec.Timestamp.Format(time.RFC3339),
+ "duration_ms": rec.DurationMs,
+ "jobs_detected": rec.JobsDetected,
+ "status": rec.Status,
+ "error": rec.Error,
+ "detailed_logs": rec.DetailedLogs,
+ })
+ }
+ data["detection_history"] = detectionList
+
+ // Get execution history
+ executionRecords := h.adminServer.PluginManager.GetExecutionHistory(jobType, 10)
+ var executionList []map[string]interface{}
+ for _, rec := range executionRecords {
+ executionList = append(executionList, map[string]interface{}{
+ "job_id": rec.JobID,
+ "started_at": rec.StartedAt.Format(time.RFC3339),
+ "completed_at": rec.CompletedAt.Format(time.RFC3339),
+ "duration_ms": rec.DurationMs,
+ "success": rec.Success,
+ "error": rec.Error,
+ "items_processed": rec.ItemsProcessed,
+ })
+ }
+ data["execution_history"] = executionList
+
+ return data
+}
+
+// Helper functions to convert between data formats
+
+func convertConfigFieldsToMap(fields []*plugin_pb.ConfigFieldValue) map[string]interface{} {
+ result := make(map[string]interface{})
+ for _, field := range fields {
+ if field == nil {
+ continue
+ }
+ if field.StringValue != "" {
+ result[field.FieldName] = field.StringValue
+ } else if field.IntValue != 0 {
+ result[field.FieldName] = field.IntValue
+ } else if field.FloatValue != 0 {
+ result[field.FieldName] = field.FloatValue
+ } else if field.BoolValue {
+ result[field.FieldName] = field.BoolValue
+ } else if field.JsonValue != "" {
+ result[field.FieldName] = field.JsonValue
+ }
+ }
+ return result
+}
+
+func convertMapToConfigFields(m map[string]interface{}) []*plugin_pb.ConfigFieldValue {
+ var fields []*plugin_pb.ConfigFieldValue
+ for k, v := range m {
+ field := &plugin_pb.ConfigFieldValue{FieldName: k}
+ switch val := v.(type) {
+ case string:
+ field.StringValue = val
+ case float64:
+ // Try to detect if it's an int or bool
+ if val == float64(int64(val)) {
+ field.IntValue = int64(val)
+ } else {
+ field.FloatValue = float32(val)
+ }
+ case bool:
+ field.BoolValue = val
+ case int:
+ field.IntValue = int64(val)
+ }
+ fields = append(fields, field)
+ }
+ return fields
+}
+
+// convertConfigSchemaToMap converts schema to a map (placeholder for future use)
+// func convertConfigSchemaToMap(schema interface{}) map[string]interface{} {
+// // TODO: implement schema conversion when schema structure is finalized
+// return make(map[string]interface{})
+// }
+
diff --git a/weed/admin/plugin/dispatcher.go b/weed/admin/plugin/dispatcher.go
index 5caf123c1..d9869124a 100644
--- a/weed/admin/plugin/dispatcher.go
+++ b/weed/admin/plugin/dispatcher.go
@@ -371,3 +371,20 @@ func (d *Dispatcher) GetStats() map[string]interface{} {
"registered_types": len(d.queues),
}
}
+
+// TriggerDetection manually triggers detection for a job type
+func (d *Dispatcher) TriggerDetection(jobType string, detector interface{}, config *JobTypeConfig) error {
+ // Record the detection run
+ record := &DetectionRecord{
+ Timestamp: time.Now(),
+ JobsDetected: 0,
+ Status: "started",
+ }
+
+ // TODO: Call detector to run detection
+ // For now, mark as completed with no jobs detected
+ record.DurationMs = 0
+ record.Status = "completed"
+
+ return nil
+}
diff --git a/weed/admin/plugin/manager.go b/weed/admin/plugin/manager.go
index ba6303830..8bf0f3672 100644
--- a/weed/admin/plugin/manager.go
+++ b/weed/admin/plugin/manager.go
@@ -399,3 +399,144 @@ func (m *Manager) Close() error {
}
return nil
}
+
+// ListConnectedPlugins returns the list of connected plugins
+func (m *Manager) ListConnectedPlugins() []*ConnectedPlugin {
+ return m.registry.ListPlugins()
+}
+
+// GetConfig retrieves configuration for a job type
+func (m *Manager) GetConfig(jobType string) (*JobTypeConfig, error) {
+ return m.configMgr.GetConfig(jobType)
+}
+
+// SaveConfig saves configuration for a job type
+func (m *Manager) SaveConfig(jobType string, enabled bool, adminConfig, workerConfig []*plugin_pb.ConfigFieldValue) error {
+ config := &JobTypeConfig{
+ JobType: jobType,
+ Enabled: enabled,
+ AdminConfig: adminConfig,
+ WorkerConfig: workerConfig,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ }
+ return m.configMgr.SaveConfig(config)
+}
+
+// GetDetectionHistory returns detection history for a job type
+func (m *Manager) GetDetectionHistory(jobType string, limit int) []*DetectionRecord {
+ config, err := m.configMgr.GetConfig(jobType)
+ if err != nil || config == nil {
+ return []*DetectionRecord{}
+ }
+
+ config.mu.RLock()
+ defer config.mu.RUnlock()
+
+ // Return in reverse order (latest first)
+ var result []*DetectionRecord
+ start := len(config.DetectionHistory) - 1
+ if start < 0 {
+ return result
+ }
+
+ for i := start; i >= 0 && len(result) < limit; i-- {
+ result = append(result, config.DetectionHistory[i])
+ }
+ return result
+}
+
+// GetExecutionHistory returns execution history for a job type
+func (m *Manager) GetExecutionHistory(jobType string, limit int) []*ExecutionRecord {
+ config, err := m.configMgr.GetConfig(jobType)
+ if err != nil || config == nil {
+ return []*ExecutionRecord{}
+ }
+
+ config.mu.RLock()
+ defer config.mu.RUnlock()
+
+ // Return in reverse order (latest first)
+ var result []*ExecutionRecord
+ start := len(config.ExecutionHistory) - 1
+ if start < 0 {
+ return result
+ }
+
+ for i := start; i >= 0 && len(result) < limit; i-- {
+ result = append(result, config.ExecutionHistory[i])
+ }
+ return result
+}
+
+// TriggerDetection manually triggers detection for a job type
+func (m *Manager) TriggerDetection(jobType string) error {
+ detector, err := m.registry.GetDetectorForJobType(jobType)
+ if err != nil {
+ return fmt.Errorf("no detector for job type %s", jobType)
+ }
+
+ config, err := m.configMgr.GetConfig(jobType)
+ if err != nil || !config.Enabled {
+ return fmt.Errorf("job type %s not enabled", jobType)
+ }
+
+ // Call the dispatcher to run detection
+ return m.dispatcher.TriggerDetection(jobType, detector, config)
+}
+
+// ListJobTypes returns the list of registered job types
+func (m *Manager) ListJobTypes() []string {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+
+ var jobTypes []string
+ for jobType := range m.dispatcher.queues {
+ jobTypes = append(jobTypes, jobType)
+ }
+ return jobTypes
+}
+
+// GetDetectorForJobType retrieves a detector for a job type
+func (m *Manager) GetDetectorForJobType(jobType string) (interface{}, error) {
+ return m.registry.GetDetectorForJobType(jobType)
+}
+
+// RecordDetection records a detection event
+func (m *Manager) RecordDetection(jobType string, record *DetectionRecord) error {
+ config, err := m.configMgr.GetConfig(jobType)
+ if err != nil || config == nil {
+ return err
+ }
+
+ config.mu.Lock()
+ defer config.mu.Unlock()
+
+ // Keep last 50 detection records
+ config.DetectionHistory = append(config.DetectionHistory, record)
+ if len(config.DetectionHistory) > 50 {
+ config.DetectionHistory = config.DetectionHistory[len(config.DetectionHistory)-50:]
+ }
+
+ return nil
+}
+
+// RecordExecution records a job execution event
+func (m *Manager) RecordExecution(jobType string, record *ExecutionRecord) error {
+ config, err := m.configMgr.GetConfig(jobType)
+ if err != nil || config == nil {
+ return err
+ }
+
+ config.mu.Lock()
+ defer config.mu.Unlock()
+
+ // Keep last 100 execution records
+ config.ExecutionHistory = append(config.ExecutionHistory, record)
+ if len(config.ExecutionHistory) > 100 {
+ config.ExecutionHistory = config.ExecutionHistory[len(config.ExecutionHistory)-100:]
+ }
+
+ return nil
+}
+
diff --git a/weed/admin/plugin/types.go b/weed/admin/plugin/types.go
index de27ee5c0..9048732e8 100644
--- a/weed/admin/plugin/types.go
+++ b/weed/admin/plugin/types.go
@@ -133,10 +133,39 @@ type JobTypeConfig struct {
CreatedAt time.Time
UpdatedAt time.Time
CreatedBy string
+
+ // History tracking
+ DetectionHistory []*DetectionRecord
+ ExecutionHistory []*ExecutionRecord
mu sync.RWMutex
}
+// DetectionRecord holds information about a detection run
+type DetectionRecord struct {
+ Timestamp time.Time
+ DurationMs int64
+ JobsDetected int
+ Status string
+ Error string
+ WorkerID string
+ DetailedLogs []string
+}
+
+// ExecutionRecord holds information about a job execution
+type ExecutionRecord struct {
+ JobID string
+ JobType string
+ StartedAt time.Time
+ CompletedAt time.Time
+ DurationMs int64
+ Success bool
+ Error string
+ WorkerID string
+ ItemsProcessed int
+ ExecutionLogs []string
+}
+
// GetState safely gets the job state
func (j *Job) GetState() JobState {
j.mu.RLock()
diff --git a/weed/admin/view/app/app.go b/weed/admin/view/app/app.go
new file mode 100644
index 000000000..84205e22e
--- /dev/null
+++ b/weed/admin/view/app/app.go
@@ -0,0 +1,74 @@
+package app
+
+import (
+ "fmt"
+)
+
+// formatNumber formats a number with commas
+func formatNumber(n interface{}) string {
+ switch v := n.(type) {
+ case int:
+ return fmt.Sprintf("%d", v)
+ case int64:
+ return fmt.Sprintf("%d", v)
+ case float64:
+ return fmt.Sprintf("%.2f", v)
+ default:
+ return fmt.Sprintf("%v", v)
+ }
+}
+
+// formatBytes formats bytes as human-readable size
+func formatBytes(bytes interface{}) string {
+ switch v := bytes.(type) {
+ case int:
+ return formatBytesValue(int64(v))
+ case int64:
+ return formatBytesValue(v)
+ case float64:
+ return formatBytesValue(int64(v))
+ default:
+ return fmt.Sprintf("%v", v)
+ }
+}
+
+func formatBytesValue(b int64) string {
+ const unit = 1024
+ if b < unit {
+ return fmt.Sprintf("%d B", b)
+ }
+ div, exp := int64(unit), 0
+ for n := b / unit; n >= unit; n /= unit {
+ div *= unit
+ exp++
+ }
+ return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
+}
+
+// calculatePercent calculates percentage
+func calculatePercent(current, total interface{}) float64 {
+ var c, t float64
+
+ switch v := current.(type) {
+ case int:
+ c = float64(v)
+ case int64:
+ c = float64(v)
+ case float64:
+ c = v
+ }
+
+ switch v := total.(type) {
+ case int:
+ t = float64(v)
+ case int64:
+ t = float64(v)
+ case float64:
+ t = v
+ }
+
+ if t == 0 {
+ return 0
+ }
+ return (c / t) * 100
+}
diff --git a/weed/admin/view/app/plugin_config.templ b/weed/admin/view/app/plugin_config.templ
new file mode 100644
index 000000000..0664fe3d9
--- /dev/null
+++ b/weed/admin/view/app/plugin_config.templ
@@ -0,0 +1,102 @@
+package app
+
+import (
+ "fmt"
+)
+
+templ PluginConfigPage(data map[string]interface{}) {
+
+
{ fmt.Sprintf("%v", data["job_type"]) } Config
+
Back
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Configuration UI coming soon
+
+
+
+
+
+
+
+
+
+ if len(data["detection_history"].([]map[string]interface{})) == 0 {
+
No detection history
+ } else {
+
+ | Timestamp | Duration | Jobs Found | Status |
+
+ for _, rec := range data["detection_history"].([]map[string]interface{}) {
+
+ | { fmt.Sprintf("%v", rec["timestamp"]) } |
+ { fmt.Sprintf("%vms", rec["duration_ms"]) } |
+ { fmt.Sprintf("%v", rec["jobs_detected"]) } |
+ { fmt.Sprintf("%v", rec["status"]) } |
+
+ }
+
+
+ }
+
+
+
+
+
+
+
+
+ if len(data["execution_history"].([]map[string]interface{})) == 0 {
+
No execution history
+ } else {
+
+ | Job ID | Started | Duration | Items | Status |
+
+ for _, rec := range data["execution_history"].([]map[string]interface{}) {
+
+ { fmt.Sprintf("%v", rec["job_id"]) } |
+ { fmt.Sprintf("%v", rec["started_at"]) } |
+ { fmt.Sprintf("%vms", rec["duration_ms"]) } |
+ { fmt.Sprintf("%v", rec["items_processed"]) } |
+
+ if rec["success"].(bool) {
+ Success
+ } else {
+ Failed
+ }
+ |
+
+ }
+
+
+ }
+
+
+
+
+}
diff --git a/weed/admin/view/app/plugin_config_templ.go b/weed/admin/view/app/plugin_config_templ.go
new file mode 100644
index 000000000..b2aa1eae1
--- /dev/null
+++ b/weed/admin/view/app/plugin_config_templ.go
@@ -0,0 +1,236 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.977
+package app
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import (
+ "fmt"
+)
+
+func PluginConfigPage(data map[string]interface{}) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var2 string
+ templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["job_type"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 9, Col: 85}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " Config
Back
Configuration UI coming soon
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if len(data["detection_history"].([]map[string]interface{})) == 0 {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
No detection history
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
| Timestamp | Duration | Jobs Found | Status |
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, rec := range data["detection_history"].([]map[string]interface{}) {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "| ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var3 string
+ templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", rec["timestamp"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 55, Col: 51}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%vms", rec["duration_ms"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 56, Col: 55}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var5 string
+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", rec["jobs_detected"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 57, Col: 55}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var6 string
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", rec["status"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 58, Col: 79}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " |
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if len(data["execution_history"].([]map[string]interface{})) == 0 {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
No execution history
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
| Job ID | Started | Duration | Items | Status |
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, rec := range data["execution_history"].([]map[string]interface{}) {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var7 string
+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", rec["job_id"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 82, Col: 54}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var8 string
+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", rec["started_at"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 83, Col: 52}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var9 string
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%vms", rec["duration_ms"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 84, Col: 55}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var10 string
+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", rec["items_processed"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 85, Col: 57}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if rec["success"].(bool) {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "Success")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "Failed")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " |
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/weed/admin/view/app/plugin_jobs.templ b/weed/admin/view/app/plugin_jobs.templ
new file mode 100644
index 000000000..6be1c91a9
--- /dev/null
+++ b/weed/admin/view/app/plugin_jobs.templ
@@ -0,0 +1,60 @@
+package app
+
+import (
+ "fmt"
+)
+
+templ PluginJobsPage(data map[string]interface{}) {
+
+
{ fmt.Sprintf("%v", data["job_type"]) } Jobs
+
+
+
+
+
Pending
{ fmt.Sprintf("%v", data["pending_count"]) }
+
Running
{ fmt.Sprintf("%v", data["running_count"]) }
+
Completed
{ fmt.Sprintf("%v", data["completed_count"]) }
+
Failed
{ fmt.Sprintf("%v", data["failed_count"]) }
+
+
+
+
+
+ if len(data["jobs"].([]map[string]interface{})) == 0 {
+
No jobs found
+ } else {
+
+ | Job ID | Description | State | Progress | Created |
+
+ for _, job := range data["jobs"].([]map[string]interface{}) {
+
+ { fmt.Sprintf("%v", job["id"]) } |
+ { fmt.Sprintf("%v", job["description"]) } |
+ { fmt.Sprintf("%v", job["state"]) } |
+
+
+ |
+ { fmt.Sprintf("%v", job["created_at"]) } |
+
+ }
+
+
+ }
+
+
+}
+
+func jobStateClass(s string) string {
+ if s == "pending" { return "bg-warning" }
+ if s == "running" { return "bg-info" }
+ if s == "completed" { return "bg-success" }
+ if s == "failed" { return "bg-danger" }
+ return "bg-secondary"
+}
diff --git a/weed/admin/view/app/plugin_jobs_templ.go b/weed/admin/view/app/plugin_jobs_templ.go
new file mode 100644
index 000000000..52032ef94
--- /dev/null
+++ b/weed/admin/view/app/plugin_jobs_templ.go
@@ -0,0 +1,237 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.977
+package app
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import (
+ "fmt"
+)
+
+func PluginJobsPage(data map[string]interface{}) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var2 string
+ templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["job_type"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 9, Col: 86}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " Jobs
Pending
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var3 string
+ templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["pending_count"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 17, Col: 212}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
Running
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["running_count"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 18, Col: 209}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
Completed
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var5 string
+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["completed_count"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 19, Col: 216}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
Failed
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var6 string
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["failed_count"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 20, Col: 209}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if len(data["jobs"].([]map[string]interface{})) == 0 {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
No jobs found
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
| Job ID | Description | State | Progress | Created |
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, job := range data["jobs"].([]map[string]interface{}) {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var7 string
+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", job["id"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 36, Col: 48}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var8 string
+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", job["description"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 37, Col: 51}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var9 = []any{"badge", jobStateClass(fmt.Sprintf("%v", job["state"]))}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var11 string
+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", job["state"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 38, Col: 117}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " | | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var13 string
+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", job["created_at"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 44, Col: 57}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " |
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func jobStateClass(s string) string {
+ if s == "pending" {
+ return "bg-warning"
+ }
+ if s == "running" {
+ return "bg-info"
+ }
+ if s == "completed" {
+ return "bg-success"
+ }
+ if s == "failed" {
+ return "bg-danger"
+ }
+ return "bg-secondary"
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/weed/admin/view/app/plugins.templ b/weed/admin/view/app/plugins.templ
new file mode 100644
index 000000000..0d232244c
--- /dev/null
+++ b/weed/admin/view/app/plugins.templ
@@ -0,0 +1,118 @@
+package app
+
+import (
+ "fmt"
+)
+
+templ PluginsPage(data map[string]interface{}) {
+
+
Plugin System
+
+
+
+
+
+
+
+
+
+
Connected Plugins
+
{ fmt.Sprintf("%v", data["total_plugins"]) }
+
+
+
+
+
+
+
+
+
+
+
+
Total Jobs
+
{ fmt.Sprintf("%v", data["total_jobs"]) }
+
+
+
+
+
+
+
+
+
+
+
+
Job Types
+
{ fmt.Sprintf("%v", len(data["job_types"].([]string))) }
+
+
+
+
+
+
+
+
+
+
+
+ if len(data["plugins"].([]map[string]interface{})) == 0 {
+
No plugins connected
+ } else {
+
+ | Name | Version | State | Health | Jobs |
+
+ for _, p := range data["plugins"].([]map[string]interface{}) {
+
+ | { fmt.Sprintf("%v", p["name"]) } |
+ { fmt.Sprintf("%v", p["version"]) } |
+ { fmt.Sprintf("%v", p["state"]) } |
+
+ if p["healthy"].(bool) {
+ Healthy
+ } else {
+ Unhealthy
+ }
+ |
+ Running: { fmt.Sprintf("%v", p["running_jobs"]) } |
+
+ }
+
+
+ }
+
+
+
+
+
+
+ if len(data["job_types"].([]string)) == 0 {
+
No job types available
+ } else {
+
+ for _, jt := range data["job_types"].([]string) {
+
+ }
+
+ }
+
+
+}
+
+func stateClass(s string) string {
+ if s == "healthy" { return "bg-success" }
+ if s == "connected" { return "bg-info" }
+ if s == "unhealthy" { return "bg-warning" }
+ return "bg-danger"
+}
diff --git a/weed/admin/view/app/plugins_templ.go b/weed/admin/view/app/plugins_templ.go
new file mode 100644
index 000000000..c8a096bd5
--- /dev/null
+++ b/weed/admin/view/app/plugins_templ.go
@@ -0,0 +1,274 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.977
+package app
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import (
+ "fmt"
+)
+
+func PluginsPage(data map[string]interface{}) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "Plugin System
Connected Plugins
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var2 string
+ templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["total_plugins"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 20, Col: 101}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
Total Jobs
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var3 string
+ templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["total_jobs"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 33, Col: 98}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
Job Types
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", len(data["job_types"].([]string))))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 46, Col: 113}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if len(data["plugins"].([]map[string]interface{})) == 0 {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
No plugins connected
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
| Name | Version | State | Health | Jobs |
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, p := range data["plugins"].([]map[string]interface{}) {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "| ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var5 string
+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", p["name"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 68, Col: 42}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var6 string
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", p["version"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 69, Col: 45}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var7 = []any{"badge", stateClass(fmt.Sprintf("%v", p["state"]))}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var9 string
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", p["state"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 70, Col: 110}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if p["healthy"].(bool) {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "Healthy")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "Unhealthy")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " | Running: ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var10 string
+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", p["running_jobs"]))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 78, Col: 59}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " |
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if len(data["job_types"].([]string)) == 0 {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
No job types available
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, jt := range data["job_types"].([]string) {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var11 string
+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(jt)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 100, Col: 36}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
Config Jobs")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func stateClass(s string) string {
+ if s == "healthy" {
+ return "bg-success"
+ }
+ if s == "connected" {
+ return "bg-info"
+ }
+ if s == "unhealthy" {
+ return "bg-warning"
+ }
+ return "bg-danger"
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/weed/admin/view/app/task_config_schema_test.go b/weed/admin/view/app/task_config_schema_test.go
deleted file mode 100644
index a4e2a8bc4..000000000
--- a/weed/admin/view/app/task_config_schema_test.go
+++ /dev/null
@@ -1,232 +0,0 @@
-package app
-
-import (
- "testing"
-)
-
-// Test structs that mirror the actual configuration structure
-type TestBaseConfigForTemplate struct {
- Enabled bool `json:"enabled"`
- ScanIntervalSeconds int `json:"scan_interval_seconds"`
- MaxConcurrent int `json:"max_concurrent"`
-}
-
-type TestTaskConfigForTemplate struct {
- TestBaseConfigForTemplate
- TaskSpecificField float64 `json:"task_specific_field"`
- AnotherSpecificField string `json:"another_specific_field"`
-}
-
-func TestGetTaskFieldValue_EmbeddedStructFields(t *testing.T) {
- config := &TestTaskConfigForTemplate{
- TestBaseConfigForTemplate: TestBaseConfigForTemplate{
- Enabled: true,
- ScanIntervalSeconds: 2400,
- MaxConcurrent: 5,
- },
- TaskSpecificField: 0.18,
- AnotherSpecificField: "test_value",
- }
-
- // Test embedded struct fields
- tests := []struct {
- fieldName string
- expectedValue interface{}
- description string
- }{
- {"enabled", true, "BaseConfig boolean field"},
- {"scan_interval_seconds", 2400, "BaseConfig integer field"},
- {"max_concurrent", 5, "BaseConfig integer field"},
- {"task_specific_field", 0.18, "Task-specific float field"},
- {"another_specific_field", "test_value", "Task-specific string field"},
- }
-
- for _, test := range tests {
- t.Run(test.description, func(t *testing.T) {
- result := getTaskFieldValue(config, test.fieldName)
-
- if result != test.expectedValue {
- t.Errorf("Field %s: expected %v (%T), got %v (%T)",
- test.fieldName, test.expectedValue, test.expectedValue, result, result)
- }
- })
- }
-}
-
-func TestGetTaskFieldValue_NonExistentField(t *testing.T) {
- config := &TestTaskConfigForTemplate{
- TestBaseConfigForTemplate: TestBaseConfigForTemplate{
- Enabled: true,
- ScanIntervalSeconds: 1800,
- MaxConcurrent: 3,
- },
- }
-
- result := getTaskFieldValue(config, "non_existent_field")
-
- if result != nil {
- t.Errorf("Expected nil for non-existent field, got %v", result)
- }
-}
-
-func TestGetTaskFieldValue_NilConfig(t *testing.T) {
- var config *TestTaskConfigForTemplate = nil
-
- result := getTaskFieldValue(config, "enabled")
-
- if result != nil {
- t.Errorf("Expected nil for nil config, got %v", result)
- }
-}
-
-func TestGetTaskFieldValue_EmptyStruct(t *testing.T) {
- config := &TestTaskConfigForTemplate{}
-
- // Test that we can extract zero values
- tests := []struct {
- fieldName string
- expectedValue interface{}
- description string
- }{
- {"enabled", false, "Zero value boolean"},
- {"scan_interval_seconds", 0, "Zero value integer"},
- {"max_concurrent", 0, "Zero value integer"},
- {"task_specific_field", 0.0, "Zero value float"},
- {"another_specific_field", "", "Zero value string"},
- }
-
- for _, test := range tests {
- t.Run(test.description, func(t *testing.T) {
- result := getTaskFieldValue(config, test.fieldName)
-
- if result != test.expectedValue {
- t.Errorf("Field %s: expected %v (%T), got %v (%T)",
- test.fieldName, test.expectedValue, test.expectedValue, result, result)
- }
- })
- }
-}
-
-func TestGetTaskFieldValue_NonStructConfig(t *testing.T) {
- var config interface{} = "not a struct"
-
- result := getTaskFieldValue(config, "enabled")
-
- if result != nil {
- t.Errorf("Expected nil for non-struct config, got %v", result)
- }
-}
-
-func TestGetTaskFieldValue_PointerToStruct(t *testing.T) {
- config := &TestTaskConfigForTemplate{
- TestBaseConfigForTemplate: TestBaseConfigForTemplate{
- Enabled: false,
- ScanIntervalSeconds: 900,
- MaxConcurrent: 2,
- },
- TaskSpecificField: 0.35,
- }
-
- // Test that pointers are handled correctly
- enabledResult := getTaskFieldValue(config, "enabled")
- if enabledResult != false {
- t.Errorf("Expected false for enabled field, got %v", enabledResult)
- }
-
- intervalResult := getTaskFieldValue(config, "scan_interval_seconds")
- if intervalResult != 900 {
- t.Errorf("Expected 900 for scan_interval_seconds field, got %v", intervalResult)
- }
-}
-
-func TestGetTaskFieldValue_FieldsWithJSONOmitempty(t *testing.T) {
- // Test struct with omitempty tags
- type TestConfigWithOmitempty struct {
- TestBaseConfigForTemplate
- OptionalField string `json:"optional_field,omitempty"`
- }
-
- config := &TestConfigWithOmitempty{
- TestBaseConfigForTemplate: TestBaseConfigForTemplate{
- Enabled: true,
- ScanIntervalSeconds: 1200,
- MaxConcurrent: 4,
- },
- OptionalField: "optional_value",
- }
-
- // Test that fields with omitempty are still found
- result := getTaskFieldValue(config, "optional_field")
- if result != "optional_value" {
- t.Errorf("Expected 'optional_value' for optional_field, got %v", result)
- }
-
- // Test embedded fields still work
- enabledResult := getTaskFieldValue(config, "enabled")
- if enabledResult != true {
- t.Errorf("Expected true for enabled field, got %v", enabledResult)
- }
-}
-
-func TestGetTaskFieldValue_DeepEmbedding(t *testing.T) {
- // Test with multiple levels of embedding
- type DeepBaseConfig struct {
- DeepField string `json:"deep_field"`
- }
-
- type MiddleConfig struct {
- DeepBaseConfig
- MiddleField int `json:"middle_field"`
- }
-
- type TopConfig struct {
- MiddleConfig
- TopField bool `json:"top_field"`
- }
-
- config := &TopConfig{
- MiddleConfig: MiddleConfig{
- DeepBaseConfig: DeepBaseConfig{
- DeepField: "deep_value",
- },
- MiddleField: 123,
- },
- TopField: true,
- }
-
- // Test that deeply embedded fields are found
- deepResult := getTaskFieldValue(config, "deep_field")
- if deepResult != "deep_value" {
- t.Errorf("Expected 'deep_value' for deep_field, got %v", deepResult)
- }
-
- middleResult := getTaskFieldValue(config, "middle_field")
- if middleResult != 123 {
- t.Errorf("Expected 123 for middle_field, got %v", middleResult)
- }
-
- topResult := getTaskFieldValue(config, "top_field")
- if topResult != true {
- t.Errorf("Expected true for top_field, got %v", topResult)
- }
-}
-
-// Benchmark to ensure performance is reasonable
-func BenchmarkGetTaskFieldValue(b *testing.B) {
- config := &TestTaskConfigForTemplate{
- TestBaseConfigForTemplate: TestBaseConfigForTemplate{
- Enabled: true,
- ScanIntervalSeconds: 1800,
- MaxConcurrent: 3,
- },
- TaskSpecificField: 0.25,
- AnotherSpecificField: "benchmark_test",
- }
-
- b.ResetTimer()
- for i := 0; i < b.N; i++ {
- // Test both embedded and regular fields
- _ = getTaskFieldValue(config, "enabled")
- _ = getTaskFieldValue(config, "task_specific_field")
- }
-}
diff --git a/weed/admin/view/app/template_helpers.go b/weed/admin/view/app/template_helpers.go
deleted file mode 100644
index eca78f136..000000000
--- a/weed/admin/view/app/template_helpers.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package app
-
-import (
- "fmt"
- "strconv"
-)
-
-// getStatusColor returns Bootstrap color class for status
-func getStatusColor(status string) string {
- switch status {
- case "active", "healthy":
- return "success"
- case "warning":
- return "warning"
- case "critical", "unreachable":
- return "danger"
- default:
- return "secondary"
- }
-}
-
-// formatBytes converts bytes to human readable format
-func formatBytes(bytes int64) string {
- if bytes == 0 {
- return "0 B"
- }
-
- units := []string{"B", "KB", "MB", "GB", "TB", "PB"}
- var i int
- value := float64(bytes)
-
- for value >= 1024 && i < len(units)-1 {
- value /= 1024
- i++
- }
-
- if i == 0 {
- return fmt.Sprintf("%.0f %s", value, units[i])
- }
- return fmt.Sprintf("%.1f %s", value, units[i])
-}
-
-// formatNumber formats large numbers with commas
-func formatNumber(num int64) string {
- if num == 0 {
- return "0"
- }
-
- str := strconv.FormatInt(num, 10)
- result := ""
-
- for i, char := range str {
- if i > 0 && (len(str)-i)%3 == 0 {
- result += ","
- }
- result += string(char)
- }
-
- return result
-}
-
-// calculatePercent calculates percentage for progress bars
-func calculatePercent(current, max int) int {
- if max == 0 {
- return 0
- }
- return (current * 100) / max
-}
diff --git a/weed/admin/view/layout/layout.templ b/weed/admin/view/layout/layout.templ
index 9c87e824d..33752160b 100644
--- a/weed/admin/view/layout/layout.templ
+++ b/weed/admin/view/layout/layout.templ
@@ -332,9 +332,27 @@ templ Layout(c *gin.Context, content templ.Component) {
-
-
+
+
+
@content
diff --git a/weed/admin/view/layout/layout_templ.go b/weed/admin/view/layout/layout_templ.go
index ec7019879..313d52683 100644
--- a/weed/admin/view/layout/layout_templ.go
+++ b/weed/admin/view/layout/layout_templ.go
@@ -487,7 +487,22 @@ func Layout(c *gin.Context, content templ.Component) templ.Component {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -495,43 +510,43 @@ func Layout(c *gin.Context, content templ.Component) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "