feat: Add admin UI, history persistence, and detailed logging for plugins

- Create plugin_handlers.go with API endpoints:
  - GET /api/plugin/list for listing plugins
  - GET/POST /api/plugin/config/* for configuration
  - GET /api/plugin/{detection,execution}/history/* for history retrieval
  - POST /api/plugin/jobs/{type}/trigger-detection for manual triggers
  - POST /api/plugin/jobs/{id}/cancel for job cancellation

- Create UI templates for plugin management:
  - plugins.templ: main plugin and job type overview
  - plugin_jobs.templ: job monitoring and status display
  - plugin_config.templ: configuration and history tabs

- Add admin menu section for plugins in layout.templ

- Update AdminServer to include PluginManager field

- Add history tracking types (DetectionRecord, ExecutionRecord) to plugin/types.go

- Add manager methods for:
  - Configuration retrieval/saving
  - Job type listing and job filtering
  - Detection and execution history retrieval
  - History recording

- Add TriggerDetection method to dispatcher

- Create helper functions in app.go for template formatting

- Fix preexisting issue with missing template helper functions
  (formatNumber, formatBytes, calculatePercent)
This commit is contained in:
Chris Lu
2026-02-17 01:31:58 -08:00
parent d51278f561
commit b0c259f6ef
17 changed files with 1898 additions and 319 deletions
+4
View File
@@ -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
+21
View File
@@ -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")
{
+533
View File
@@ -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{})
// }
+17
View File
@@ -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
}
+141
View File
@@ -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
}
+29
View File
@@ -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()
+74
View File
@@ -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
}
+102
View File
@@ -0,0 +1,102 @@
package app
import (
"fmt"
)
templ PluginConfigPage(data map[string]interface{}) {
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="fas fa-cog me-2"></i>{ fmt.Sprintf("%v", data["job_type"]) } Config</h1>
<a href="/plugins" class="btn btn-sm btn-secondary">Back</a>
</div>
<ul class="nav nav-tabs mb-4" role="tablist">
<li class="nav-item"><button class="nav-link active" id="config-tab" data-bs-toggle="tab" data-bs-target="#config" type="button">Configuration</button></li>
<li class="nav-item"><button class="nav-link" id="detection-tab" data-bs-toggle="tab" data-bs-target="#detection" type="button">Detection History</button></li>
<li class="nav-item"><button class="nav-link" id="execution-tab" data-bs-toggle="tab" data-bs-target="#execution" type="button">Execution History</button></li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="config" role="tabpanel">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Configuration</h6>
</div>
<div class="card-body">
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="enabled" checked?={ data["enabled"].(bool) }>
<label class="form-check-label" for="enabled">Enabled</label>
</div>
</div>
<div class="alert alert-info"><i class="fas fa-info-circle me-2"></i>Configuration UI coming soon</div>
<button type="button" class="btn btn-primary">Save</button>
</div>
</div>
</div>
<div class="tab-pane fade" id="detection" role="tabpanel">
<div class="card shadow">
<div class="card-header py-3">
<div class="d-flex justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">Detection History</h6>
<button type="button" class="btn btn-sm btn-primary">Run Detection</button>
</div>
</div>
<div class="card-body">
if len(data["detection_history"].([]map[string]interface{})) == 0 {
<div class="alert alert-info">No detection history</div>
} else {
<table class="table table-sm">
<thead><tr><th>Timestamp</th><th>Duration</th><th>Jobs Found</th><th>Status</th></tr></thead>
<tbody>
for _, rec := range data["detection_history"].([]map[string]interface{}) {
<tr>
<td>{ fmt.Sprintf("%v", rec["timestamp"]) }</td>
<td>{ fmt.Sprintf("%vms", rec["duration_ms"]) }</td>
<td>{ fmt.Sprintf("%v", rec["jobs_detected"]) }</td>
<td><span class="badge bg-success">{ fmt.Sprintf("%v", rec["status"]) }</span></td>
</tr>
}
</tbody>
</table>
}
</div>
</div>
</div>
<div class="tab-pane fade" id="execution" role="tabpanel">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Execution History</h6>
</div>
<div class="card-body">
if len(data["execution_history"].([]map[string]interface{})) == 0 {
<div class="alert alert-info">No execution history</div>
} else {
<table class="table table-sm">
<thead><tr><th>Job ID</th><th>Started</th><th>Duration</th><th>Items</th><th>Status</th></tr></thead>
<tbody>
for _, rec := range data["execution_history"].([]map[string]interface{}) {
<tr>
<td><code>{ fmt.Sprintf("%v", rec["job_id"]) }</code></td>
<td>{ fmt.Sprintf("%v", rec["started_at"]) }</td>
<td>{ fmt.Sprintf("%vms", rec["duration_ms"]) }</td>
<td>{ fmt.Sprintf("%v", rec["items_processed"]) }</td>
<td>
if rec["success"].(bool) {
<span class="badge bg-success">Success</span>
} else {
<span class="badge bg-danger">Failed</span>
}
</td>
</tr>
}
</tbody>
</table>
}
</div>
</div>
</div>
</div>
}
+236
View File
@@ -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, "<div class=\"d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom\"><h1 class=\"h2\"><i class=\"fas fa-cog me-2\"></i>")
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</h1><a href=\"/plugins\" class=\"btn btn-sm btn-secondary\">Back</a></div><ul class=\"nav nav-tabs mb-4\" role=\"tablist\"><li class=\"nav-item\"><button class=\"nav-link active\" id=\"config-tab\" data-bs-toggle=\"tab\" data-bs-target=\"#config\" type=\"button\">Configuration</button></li><li class=\"nav-item\"><button class=\"nav-link\" id=\"detection-tab\" data-bs-toggle=\"tab\" data-bs-target=\"#detection\" type=\"button\">Detection History</button></li><li class=\"nav-item\"><button class=\"nav-link\" id=\"execution-tab\" data-bs-toggle=\"tab\" data-bs-target=\"#execution\" type=\"button\">Execution History</button></li></ul><div class=\"tab-content\"><div class=\"tab-pane fade show active\" id=\"config\" role=\"tabpanel\"><div class=\"card shadow\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\">Configuration</h6></div><div class=\"card-body\"><div class=\"mb-3\"><div class=\"form-check form-switch\"><input class=\"form-check-input\" type=\"checkbox\" id=\"enabled\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data["enabled"].(bool) {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " checked")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "> <label class=\"form-check-label\" for=\"enabled\">Enabled</label></div></div><div class=\"alert alert-info\"><i class=\"fas fa-info-circle me-2\"></i>Configuration UI coming soon</div><button type=\"button\" class=\"btn btn-primary\">Save</button></div></div></div><div class=\"tab-pane fade\" id=\"detection\" role=\"tabpanel\"><div class=\"card shadow\"><div class=\"card-header py-3\"><div class=\"d-flex justify-content-between\"><h6 class=\"m-0 font-weight-bold text-primary\">Detection History</h6><button type=\"button\" class=\"btn btn-sm btn-primary\">Run Detection</button></div></div><div class=\"card-body\">")
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, "<div class=\"alert alert-info\">No detection history</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table class=\"table table-sm\"><thead><tr><th>Timestamp</th><th>Duration</th><th>Jobs Found</th><th>Status</th></tr></thead> <tbody>")
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, "<tr><td>")
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, "</td><td>")
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, "</td><td>")
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, "</td><td><span class=\"badge bg-success\">")
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, "</span></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</tbody></table>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div></div></div><div class=\"tab-pane fade\" id=\"execution\" role=\"tabpanel\"><div class=\"card shadow\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\">Execution History</h6></div><div class=\"card-body\">")
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, "<div class=\"alert alert-info\">No execution history</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<table class=\"table table-sm\"><thead><tr><th>Job ID</th><th>Started</th><th>Duration</th><th>Items</th><th>Status</th></tr></thead> <tbody>")
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, "<tr><td><code>")
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, "</code></td><td>")
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, "</td><td>")
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, "</td><td>")
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, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if rec["success"].(bool) {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<span class=\"badge bg-success\">Success</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<span class=\"badge bg-danger\">Failed</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</tbody></table>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
+60
View File
@@ -0,0 +1,60 @@
package app
import (
"fmt"
)
templ PluginJobsPage(data map[string]interface{}) {
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="fas fa-list me-2"></i>{ fmt.Sprintf("%v", data["job_type"]) } Jobs</h1>
<div>
<a href="/plugins" class="btn btn-sm btn-secondary me-2">Back</a>
<button type="button" class="btn btn-sm btn-primary" onclick="location.reload();">Refresh</button>
</div>
</div>
<div class="row mb-4">
<div class="col-lg-3"><div class="card"><div class="card-body"><div class="text-xs font-weight-bold text-warning text-uppercase mb-1">Pending</div><div class="h5 mb-0">{ fmt.Sprintf("%v", data["pending_count"]) }</div></div></div></div>
<div class="col-lg-3"><div class="card"><div class="card-body"><div class="text-xs font-weight-bold text-info text-uppercase mb-1">Running</div><div class="h5 mb-0">{ fmt.Sprintf("%v", data["running_count"]) }</div></div></div></div>
<div class="col-lg-3"><div class="card"><div class="card-body"><div class="text-xs font-weight-bold text-success text-uppercase mb-1">Completed</div><div class="h5 mb-0">{ fmt.Sprintf("%v", data["completed_count"]) }</div></div></div></div>
<div class="col-lg-3"><div class="card"><div class="card-body"><div class="text-xs font-weight-bold text-danger text-uppercase mb-1">Failed</div><div class="h5 mb-0">{ fmt.Sprintf("%v", data["failed_count"]) }</div></div></div></div>
</div>
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Job List</h6>
</div>
<div class="card-body">
if len(data["jobs"].([]map[string]interface{})) == 0 {
<div class="alert alert-info">No jobs found</div>
} else {
<table class="table table-sm table-hover">
<thead><tr><th>Job ID</th><th>Description</th><th>State</th><th>Progress</th><th>Created</th></tr></thead>
<tbody>
for _, job := range data["jobs"].([]map[string]interface{}) {
<tr>
<td><code>{ fmt.Sprintf("%v", job["id"]) }</code></td>
<td>{ fmt.Sprintf("%v", job["description"]) }</td>
<td><span class={ "badge", jobStateClass(fmt.Sprintf("%v", job["state"])) }>{ fmt.Sprintf("%v", job["state"]) }</span></td>
<td>
<div class="progress" style="height: 20px;">
<div class="progress-bar" style={ "width: " + fmt.Sprintf("%v%%", job["progress"]) }></div>
</div>
</td>
<td><small>{ fmt.Sprintf("%v", job["created_at"]) }</small></td>
</tr>
}
</tbody>
</table>
}
</div>
</div>
}
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"
}
+237
View File
@@ -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, "<div class=\"d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom\"><h1 class=\"h2\"><i class=\"fas fa-list me-2\"></i>")
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</h1><div><a href=\"/plugins\" class=\"btn btn-sm btn-secondary me-2\">Back</a> <button type=\"button\" class=\"btn btn-sm btn-primary\" onclick=\"location.reload();\">Refresh</button></div></div><div class=\"row mb-4\"><div class=\"col-lg-3\"><div class=\"card\"><div class=\"card-body\"><div class=\"text-xs font-weight-bold text-warning text-uppercase mb-1\">Pending</div><div class=\"h5 mb-0\">")
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, "</div></div></div></div><div class=\"col-lg-3\"><div class=\"card\"><div class=\"card-body\"><div class=\"text-xs font-weight-bold text-info text-uppercase mb-1\">Running</div><div class=\"h5 mb-0\">")
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, "</div></div></div></div><div class=\"col-lg-3\"><div class=\"card\"><div class=\"card-body\"><div class=\"text-xs font-weight-bold text-success text-uppercase mb-1\">Completed</div><div class=\"h5 mb-0\">")
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, "</div></div></div></div><div class=\"col-lg-3\"><div class=\"card\"><div class=\"card-body\"><div class=\"text-xs font-weight-bold text-danger text-uppercase mb-1\">Failed</div><div class=\"h5 mb-0\">")
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, "</div></div></div></div></div><div class=\"card shadow\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\">Job List</h6></div><div class=\"card-body\">")
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, "<div class=\"alert alert-info\">No jobs found</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<table class=\"table table-sm table-hover\"><thead><tr><th>Job ID</th><th>Description</th><th>State</th><th>Progress</th><th>Created</th></tr></thead> <tbody>")
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, "<tr><td><code>")
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, "</code></td><td>")
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, "</td><td>")
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, "<span class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 1, Col: 0}
}
_, 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, 13, "\">")
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, "</span></td><td><div class=\"progress\" style=\"height: 20px;\"><div class=\"progress-bar\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("width: " + fmt.Sprintf("%v%%", job["progress"]))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 41, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"></div></div></td><td><small>")
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, "</small></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</tbody></table>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div></div>")
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
+118
View File
@@ -0,0 +1,118 @@
package app
import (
"fmt"
)
templ PluginsPage(data map[string]interface{}) {
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="fas fa-puzzle-piece me-2"></i>Plugin System</h1>
<button type="button" class="btn btn-sm btn-primary" onclick="location.reload();"><i class="fas fa-sync me-1"></i>Refresh</button>
</div>
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">Connected Plugins</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{ fmt.Sprintf("%v", data["total_plugins"]) }</div>
</div>
<div class="col-auto"><i class="fas fa-plug fa-2x text-gray-300"></i></div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">Total Jobs</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{ fmt.Sprintf("%v", data["total_jobs"]) }</div>
</div>
<div class="col-auto"><i class="fas fa-tasks fa-2x text-gray-300"></i></div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">Job Types</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{ fmt.Sprintf("%v", len(data["job_types"].([]string))) }</div>
</div>
<div class="col-auto"><i class="fas fa-cubes fa-2x text-gray-300"></i></div>
</div>
</div>
</div>
</div>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary"><i class="fas fa-plug me-2"></i>Connected Plugins</h6>
</div>
<div class="card-body">
if len(data["plugins"].([]map[string]interface{})) == 0 {
<div class="alert alert-info"><i class="fas fa-info-circle me-2"></i>No plugins connected</div>
} else {
<table class="table table-hover table-sm">
<thead><tr><th>Name</th><th>Version</th><th>State</th><th>Health</th><th>Jobs</th></tr></thead>
<tbody>
for _, p := range data["plugins"].([]map[string]interface{}) {
<tr>
<td>{ fmt.Sprintf("%v", p["name"]) }</td>
<td>{ fmt.Sprintf("%v", p["version"]) }</td>
<td><span class={ "badge", stateClass(fmt.Sprintf("%v", p["state"])) }>{ fmt.Sprintf("%v", p["state"]) }</span></td>
<td>
if p["healthy"].(bool) {
<span class="badge bg-success">Healthy</span>
} else {
<span class="badge bg-danger">Unhealthy</span>
}
</td>
<td>Running: { fmt.Sprintf("%v", p["running_jobs"]) }</td>
</tr>
}
</tbody>
</table>
}
</div>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary"><i class="fas fa-cubes me-2"></i>Job Types</h6>
</div>
<div class="card-body">
if len(data["job_types"].([]string)) == 0 {
<div class="alert alert-info">No job types available</div>
} else {
<div class="row">
for _, jt := range data["job_types"].([]string) {
<div class="col-lg-4 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">{ jt }</h5>
<a href={ templ.SafeURL("/plugins/config/" + jt) } class="btn btn-sm btn-primary">Config</a>
<a href={ templ.SafeURL("/plugins/jobs/" + jt) } class="btn btn-sm btn-info">Jobs</a>
</div>
</div>
</div>
}
</div>
}
</div>
</div>
}
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"
}
+274
View File
@@ -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, "<div class=\"d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom\"><h1 class=\"h2\"><i class=\"fas fa-puzzle-piece me-2\"></i>Plugin System</h1><button type=\"button\" class=\"btn btn-sm btn-primary\" onclick=\"location.reload();\"><i class=\"fas fa-sync me-1\"></i>Refresh</button></div><div class=\"row mb-4\"><div class=\"col-xl-3 col-md-6 mb-4\"><div class=\"card border-left-primary shadow h-100 py-2\"><div class=\"card-body\"><div class=\"row no-gutters align-items-center\"><div class=\"col mr-2\"><div class=\"text-xs font-weight-bold text-primary text-uppercase mb-1\">Connected Plugins</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">")
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, "</div></div><div class=\"col-auto\"><i class=\"fas fa-plug fa-2x text-gray-300\"></i></div></div></div></div></div><div class=\"col-xl-3 col-md-6 mb-4\"><div class=\"card border-left-success shadow h-100 py-2\"><div class=\"card-body\"><div class=\"row no-gutters align-items-center\"><div class=\"col mr-2\"><div class=\"text-xs font-weight-bold text-success text-uppercase mb-1\">Total Jobs</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">")
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, "</div></div><div class=\"col-auto\"><i class=\"fas fa-tasks fa-2x text-gray-300\"></i></div></div></div></div></div><div class=\"col-xl-3 col-md-6 mb-4\"><div class=\"card border-left-info shadow h-100 py-2\"><div class=\"card-body\"><div class=\"row no-gutters align-items-center\"><div class=\"col mr-2\"><div class=\"text-xs font-weight-bold text-info text-uppercase mb-1\">Job Types</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">")
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, "</div></div><div class=\"col-auto\"><i class=\"fas fa-cubes fa-2x text-gray-300\"></i></div></div></div></div></div></div><div class=\"card shadow mb-4\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-plug me-2\"></i>Connected Plugins</h6></div><div class=\"card-body\">")
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, "<div class=\"alert alert-info\"><i class=\"fas fa-info-circle me-2\"></i>No plugins connected</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table class=\"table table-hover table-sm\"><thead><tr><th>Name</th><th>Version</th><th>State</th><th>Health</th><th>Jobs</th></tr></thead> <tbody>")
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, "<tr><td>")
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, "</td><td>")
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, "</td><td>")
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, "<span class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var7).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 1, Col: 0}
}
_, 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 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, "</span></td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if p["healthy"].(bool) {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span class=\"badge bg-success\">Healthy</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span class=\"badge bg-danger\">Unhealthy</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</td><td>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, "</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</tbody></table>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div></div><div class=\"card shadow mb-4\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-cubes me-2\"></i>Job Types</h6></div><div class=\"card-body\">")
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, "<div class=\"alert alert-info\">No job types available</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div class=\"row\">")
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, "<div class=\"col-lg-4 mb-3\"><div class=\"card\"><div class=\"card-body\"><h5 class=\"card-title\">")
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, "</h5><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 templ.SafeURL
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("/plugins/config/" + jt))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 101, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" class=\"btn btn-sm btn-primary\">Config</a> <a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 templ.SafeURL
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("/plugins/jobs/" + jt))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 102, Col: 55}
}
_, 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, 24, "\" class=\"btn btn-sm btn-info\">Jobs</a></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div></div>")
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
@@ -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")
}
}
-68
View File
@@ -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
}
+20 -2
View File
@@ -332,9 +332,27 @@ templ Layout(c *gin.Context, content templ.Component) {
</li>
</ul>
</div>
</div>
<!-- Main content -->
<!-- PLUGIN SECTION -->
<div class="sidebar-menu-group">
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted">
<span>PLUGINS</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
if currentPath == "/plugins" {
<a class="nav-link active" href="/plugins">
<i class="fas fa-puzzle-piece me-2"></i>Plugins
</a>
} else {
<a class="nav-link" href="/plugins">
<i class="fas fa-puzzle-piece me-2"></i>Plugins
</a>
}
</li>
</ul>
</div>
</div>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<div class="pt-3">
@content
+32 -17
View File
@@ -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, "</li></ul></div></div><!-- Main content --><main class=\"col-md-9 ms-sm-auto col-lg-10 px-md-4\"><div class=\"pt-3\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</li></ul></div><!-- PLUGIN SECTION --><div class=\"sidebar-menu-group\"><h6 class=\"sidebar-heading px-3 mt-4 mb-1 text-muted\"><span>PLUGINS</span></h6><ul class=\"nav flex-column\"><li class=\"nav-item\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if currentPath == "/plugins" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<a class=\"nav-link active\" href=\"/plugins\"><i class=\"fas fa-puzzle-piece me-2\"></i>Plugins</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<a class=\"nav-link\" href=\"/plugins\"><i class=\"fas fa-puzzle-piece me-2\"></i>Plugins</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</li></ul></div></div><main class=\"col-md-9 ms-sm-auto col-lg-10 px-md-4\"><div class=\"pt-3\">")
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, "</div></main></div></div><!-- Footer --><footer class=\"footer mt-auto py-3 bg-light\"><div class=\"container-fluid text-center\"><small class=\"text-muted\">&copy; ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</div></main></div></div><!-- Footer --><footer class=\"footer mt-auto py-3 bg-light\"><div class=\"container-fluid text-center\"><small class=\"text-muted\">&copy; ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", time.Now().Year()))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 350, Col: 60}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 368, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, " SeaweedFS Admin v")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, " SeaweedFS Admin v")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(version.VERSION_NUMBER)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 350, Col: 102}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 368, Col: 102}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, " ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !strings.Contains(version.VERSION, "enterprise") {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<span class=\"mx-2\">•</span> <a href=\"https://seaweedfs.com\" target=\"_blank\" class=\"text-decoration-none\"><i class=\"fas fa-star me-1\"></i>Enterprise Version Available</a>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<span class=\"mx-2\">•</span> <a href=\"https://seaweedfs.com\" target=\"_blank\" class=\"text-decoration-none\"><i class=\"fas fa-star me-1\"></i>Enterprise Version Available</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</small></div></footer><!-- Bootstrap JS --><script src=\"/static/js/bootstrap.bundle.min.js\"></script><!-- Modal Alerts JS (replaces native alert/confirm) --><script src=\"/static/js/modal-alerts.js\"></script><!-- Custom JS --><script src=\"/static/js/admin.js\"></script><script src=\"/static/js/iam-utils.js\"></script><script src=\"/static/js/s3tables.js\"></script></body></html>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</small></div></footer><!-- Bootstrap JS --><script src=\"/static/js/bootstrap.bundle.min.js\"></script><!-- Modal Alerts JS (replaces native alert/confirm) --><script src=\"/static/js/modal-alerts.js\"></script><!-- Custom JS --><script src=\"/static/js/admin.js\"></script><script src=\"/static/js/iam-utils.js\"></script><script src=\"/static/js/s3tables.js\"></script></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -560,56 +575,56 @@ func LoginForm(c *gin.Context, title string, errorMessage string) templ.Componen
templ_7745c5c3_Var28 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 378, Col: 17}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 396, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " - Login</title><link rel=\"icon\" href=\"/static/favicon.ico\" type=\"image/x-icon\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"/static/css/bootstrap.min.css\" rel=\"stylesheet\"><link href=\"/static/css/fontawesome.min.css\" rel=\"stylesheet\"></head><body class=\"bg-light\"><div class=\"container\"><div class=\"row justify-content-center min-vh-100 align-items-center\"><div class=\"col-md-6 col-lg-4\"><div class=\"card shadow\"><div class=\"card-body p-5\"><div class=\"text-center mb-4\"><i class=\"fas fa-server fa-3x text-primary mb-3\"></i><h4 class=\"card-title\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " - Login</title><link rel=\"icon\" href=\"/static/favicon.ico\" type=\"image/x-icon\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"/static/css/bootstrap.min.css\" rel=\"stylesheet\"><link href=\"/static/css/fontawesome.min.css\" rel=\"stylesheet\"></head><body class=\"bg-light\"><div class=\"container\"><div class=\"row justify-content-center min-vh-100 align-items-center\"><div class=\"col-md-6 col-lg-4\"><div class=\"card shadow\"><div class=\"card-body p-5\"><div class=\"text-center mb-4\"><i class=\"fas fa-server fa-3x text-primary mb-3\"></i><h4 class=\"card-title\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 392, Col: 57}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 410, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</h4><p class=\"text-muted\">Please sign in to continue</p></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</h4><p class=\"text-muted\">Please sign in to continue</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if errorMessage != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<div class=\"alert alert-danger\" role=\"alert\"><i class=\"fas fa-exclamation-triangle me-2\"></i> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<div class=\"alert alert-danger\" role=\"alert\"><i class=\"fas fa-exclamation-triangle me-2\"></i> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 399, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 417, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<form method=\"POST\" action=\"/login\"><div class=\"mb-3\"><label for=\"username\" class=\"form-label\">Username</label><div class=\"input-group\"><span class=\"input-group-text\"><i class=\"fas fa-user\"></i></span> <input type=\"text\" class=\"form-control\" id=\"username\" name=\"username\" required></div></div><div class=\"mb-4\"><label for=\"password\" class=\"form-label\">Password</label><div class=\"input-group\"><span class=\"input-group-text\"><i class=\"fas fa-lock\"></i></span> <input type=\"password\" class=\"form-control\" id=\"password\" name=\"password\" required></div></div><button type=\"submit\" class=\"btn btn-primary w-100\"><i class=\"fas fa-sign-in-alt me-2\"></i>Sign In</button></form></div></div></div></div></div><script src=\"/static/js/bootstrap.bundle.min.js\"></script></body></html>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<form method=\"POST\" action=\"/login\"><div class=\"mb-3\"><label for=\"username\" class=\"form-label\">Username</label><div class=\"input-group\"><span class=\"input-group-text\"><i class=\"fas fa-user\"></i></span> <input type=\"text\" class=\"form-control\" id=\"username\" name=\"username\" required></div></div><div class=\"mb-4\"><label for=\"password\" class=\"form-label\">Password</label><div class=\"input-group\"><span class=\"input-group-text\"><i class=\"fas fa-lock\"></i></span> <input type=\"password\" class=\"form-control\" id=\"password\" name=\"password\" required></div></div><button type=\"submit\" class=\"btn btn-primary w-100\"><i class=\"fas fa-sign-in-alt me-2\"></i>Sign In</button></form></div></div></div></div></div><script src=\"/static/js/bootstrap.bundle.min.js\"></script></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}