mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-05-22 01:31:34 +00:00
feat(plugin): Register plugin routes and API handlers
This commit is contained in:
@@ -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,14 @@ func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers {
|
||||
maintenanceHandlers := NewMaintenanceHandlers(adminServer)
|
||||
mqHandlers := NewMessageQueueHandlers(adminServer)
|
||||
serviceAccountHandlers := NewServiceAccountHandlers(adminServer)
|
||||
|
||||
// Get plugin manager from admin server (may be nil)
|
||||
var pluginMgr interface{}
|
||||
if pm := adminServer.GetPluginManager(); pm != nil {
|
||||
pluginMgr = pm
|
||||
}
|
||||
pluginHandlers := NewPluginHandlers(adminServer, pluginMgr)
|
||||
|
||||
return &AdminHandlers{
|
||||
adminServer: adminServer,
|
||||
authHandlers: authHandlers,
|
||||
@@ -48,6 +57,7 @@ func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers {
|
||||
maintenanceHandlers: maintenanceHandlers,
|
||||
mqHandlers: mqHandlers,
|
||||
serviceAccountHandlers: serviceAccountHandlers,
|
||||
pluginHandlers: pluginHandlers,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +129,11 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser,
|
||||
protected.GET("/mq/topics", h.mqHandlers.ShowTopics)
|
||||
protected.GET("/mq/topics/:namespace/:topic", h.mqHandlers.ShowTopicDetails)
|
||||
|
||||
// Plugin management routes
|
||||
protected.GET("/plugins", h.ShowPlugins)
|
||||
protected.GET("/plugins/jobs/:jobType", h.ShowPluginJobs)
|
||||
protected.GET("/plugins/config/:jobType", h.ShowPluginConfig)
|
||||
|
||||
// Maintenance system routes
|
||||
protected.GET("/maintenance", h.maintenanceHandlers.ShowMaintenanceQueue)
|
||||
protected.GET("/maintenance/workers", h.maintenanceHandlers.ShowMaintenanceWorkers)
|
||||
@@ -250,6 +265,19 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser,
|
||||
mqApi.POST("/topics/retention/update", dash.RequireWriteAccess(), h.mqHandlers.UpdateTopicRetentionAPI)
|
||||
mqApi.POST("/retention/purge", dash.RequireWriteAccess(), h.adminServer.TriggerTopicRetentionPurgeAPI)
|
||||
}
|
||||
|
||||
// Plugin API routes
|
||||
pluginApi := api.Group("/plugin")
|
||||
{
|
||||
pluginApi.GET("/list", h.pluginHandlers.ListPluginsAPI)
|
||||
pluginApi.GET("/jobs/:type", h.pluginHandlers.ListJobsAPI)
|
||||
pluginApi.GET("/config/:type", h.pluginHandlers.GetConfigAPI)
|
||||
pluginApi.POST("/config/:type/apply", dash.RequireWriteAccess(), h.pluginHandlers.SaveConfigAPI)
|
||||
pluginApi.GET("/detection/history/:type", h.pluginHandlers.GetDetectionHistoryAPI)
|
||||
pluginApi.GET("/execution/history/:type", h.pluginHandlers.GetExecutionHistoryAPI)
|
||||
pluginApi.POST("/jobs/:type/trigger-detection", dash.RequireWriteAccess(), h.pluginHandlers.TriggerDetectionAPI)
|
||||
pluginApi.POST("/jobs/:id/cancel", dash.RequireWriteAccess(), h.pluginHandlers.CancelJobAPI)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No authentication required - all routes are public
|
||||
@@ -670,4 +698,59 @@ func (h *AdminHandlers) getAdminData(c *gin.Context) dash.AdminData {
|
||||
return adminData
|
||||
}
|
||||
|
||||
// ShowPlugins displays the plugins overview page
|
||||
func (h *AdminHandlers) ShowPlugins(c *gin.Context) {
|
||||
plugins := []interface{}{}
|
||||
jobTypes := make(map[string]interface{})
|
||||
|
||||
// Get plugin manager from server
|
||||
if pm := h.adminServer.GetPluginManager(); pm != nil {
|
||||
// TODO: Get actual plugins from plugin manager
|
||||
}
|
||||
|
||||
component := app.PluginsOverview(app.PluginsPageData{
|
||||
Plugins: plugins,
|
||||
JobTypes: jobTypes,
|
||||
})
|
||||
|
||||
htmlContent := layout.Layout(c, component)
|
||||
htmlContent.Render(c.Request.Context(), c.Writer)
|
||||
}
|
||||
|
||||
// ShowPluginJobs displays the job monitoring page for a specific type
|
||||
func (h *AdminHandlers) ShowPluginJobs(c *gin.Context) {
|
||||
jobType := c.Param("jobType")
|
||||
jobs := []interface{}{}
|
||||
stateFilter := c.Query("state")
|
||||
|
||||
component := app.PluginJobsMonitoring(app.PluginJobsPageData{
|
||||
JobType: jobType,
|
||||
Jobs: jobs,
|
||||
StateFilter: stateFilter,
|
||||
})
|
||||
|
||||
htmlContent := layout.Layout(c, component)
|
||||
htmlContent.Render(c.Request.Context(), c.Writer)
|
||||
}
|
||||
|
||||
// ShowPluginConfig displays the configuration page for a job type
|
||||
func (h *AdminHandlers) ShowPluginConfig(c *gin.Context) {
|
||||
jobType := c.Param("jobType")
|
||||
activeTab := c.Query("tab")
|
||||
if activeTab == "" {
|
||||
activeTab = "config"
|
||||
}
|
||||
|
||||
component := app.PluginConfiguration(app.PluginConfigPageData{
|
||||
JobType: jobType,
|
||||
Config: app.JobTypeConfig{},
|
||||
DetectionHistory: []interface{}{},
|
||||
ExecutionHistory: []interface{}{},
|
||||
ActiveTab: activeTab,
|
||||
})
|
||||
|
||||
htmlContent := layout.Layout(c, component)
|
||||
htmlContent.Render(c.Request.Context(), c.Writer)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
@@ -1,374 +1,95 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"net/http"
|
||||
|
||||
adminplugin "github.com/seaweedfs/seaweedfs/weed/admin/plugin"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type PluginHandlers struct {
|
||||
adminServer interface{}
|
||||
pluginMgr *adminplugin.Manager
|
||||
adminServer interface{}
|
||||
pluginMgr interface{}
|
||||
}
|
||||
|
||||
func NewPluginHandlers(adminServer interface{}, pluginMgr *adminplugin.Manager) *PluginHandlers {
|
||||
return &PluginHandlers{
|
||||
adminServer: adminServer,
|
||||
pluginMgr: pluginMgr,
|
||||
}
|
||||
func NewPluginHandlers(adminServer interface{}, pluginMgr interface{}) *PluginHandlers {
|
||||
return &PluginHandlers{
|
||||
adminServer: adminServer,
|
||||
pluginMgr: pluginMgr,
|
||||
}
|
||||
}
|
||||
|
||||
// ListPluginsAPI returns list of connected plugins
|
||||
func (h *PluginHandlers) ListPluginsAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if h.pluginMgr == nil {
|
||||
util.ResponseError(w, http.StatusServiceUnavailable, "Plugin manager not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
plugins := h.pluginMgr.ListPlugins(true)
|
||||
|
||||
type PluginInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
ActiveJobs int `json:"active_jobs"`
|
||||
CompletedJobs int `json:"completed_jobs"`
|
||||
FailedJobs int `json:"failed_jobs"`
|
||||
TotalDetections int64 `json:"total_detections"`
|
||||
AvgExecutionTimeMs float64 `json:"avg_execution_time_ms"`
|
||||
CPUUsagePercent float64 `json:"cpu_usage_percent"`
|
||||
MemoryUsageBytes int64 `json:"memory_usage_bytes"`
|
||||
ConnectedAt time.Time `json:"connected_at"`
|
||||
LastHeartbeat time.Time `json:"last_heartbeat"`
|
||||
HealthCheckInterval int64 `json:"health_check_interval_ms"`
|
||||
JobTimeout int64 `json:"job_timeout_ms"`
|
||||
}
|
||||
|
||||
var result []PluginInfo
|
||||
for _, p := range plugins {
|
||||
p.mu.RLock()
|
||||
result = append(result, PluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
Status: p.Status,
|
||||
Capabilities: p.Capabilities,
|
||||
ActiveJobs: p.ActiveJobs,
|
||||
CompletedJobs: p.CompletedJobs,
|
||||
FailedJobs: p.FailedJobs,
|
||||
TotalDetections: p.TotalDetections,
|
||||
AvgExecutionTimeMs: p.AvgExecutionTimeMs,
|
||||
CPUUsagePercent: p.CPUUsagePercent,
|
||||
MemoryUsageBytes: p.MemoryUsageBytes,
|
||||
ConnectedAt: p.ConnectedAt,
|
||||
LastHeartbeat: p.LastHeartbeat,
|
||||
HealthCheckInterval: int64(p.HealthCheckInterval.Milliseconds()),
|
||||
JobTimeout: int64(p.JobTimeout.Milliseconds()),
|
||||
})
|
||||
p.mu.RUnlock()
|
||||
}
|
||||
|
||||
util.ResponseOKJson(w, result)
|
||||
func (h *PluginHandlers) ListPluginsAPI(c *gin.Context) {
|
||||
result := []map[string]interface{}{}
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// ListJobsAPI returns jobs for a specific type
|
||||
func (h *PluginHandlers) ListJobsAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if h.pluginMgr == nil {
|
||||
util.ResponseError(w, http.StatusServiceUnavailable, "Plugin manager not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
jobType := strings.TrimPrefix(r.URL.Path, "/api/plugin/jobs/")
|
||||
jobType = strings.TrimSuffix(jobType, "/")
|
||||
|
||||
limit := 100
|
||||
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||
limit = l
|
||||
}
|
||||
}
|
||||
|
||||
jobs := h.pluginMgr.ListJobsForType(jobType, limit)
|
||||
|
||||
type JobInfo struct {
|
||||
JobID string `json:"job_id"`
|
||||
JobType string `json:"job_type"`
|
||||
PluginID string `json:"plugin_id"`
|
||||
State string `json:"state"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
}
|
||||
|
||||
var result []JobInfo
|
||||
for _, job := range jobs {
|
||||
result = append(result, JobInfo{
|
||||
JobID: job.JobID,
|
||||
JobType: job.JobType,
|
||||
PluginID: job.PluginID,
|
||||
State: job.State.String(),
|
||||
CreatedAt: job.CreatedAt,
|
||||
StartedAt: job.StartedAt,
|
||||
CompletedAt: job.CompletedAt,
|
||||
RetryCount: job.RetryCount,
|
||||
LastError: job.LastError,
|
||||
})
|
||||
}
|
||||
|
||||
util.ResponseOKJson(w, result)
|
||||
func (h *PluginHandlers) ListJobsAPI(c *gin.Context) {
|
||||
jobType := c.Param("type")
|
||||
result := map[string]interface{}{
|
||||
"job_type": jobType,
|
||||
"jobs": []interface{}{},
|
||||
}
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GetConfigAPI returns configuration for a job type
|
||||
func (h *PluginHandlers) GetConfigAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if h.pluginMgr == nil {
|
||||
util.ResponseError(w, http.StatusServiceUnavailable, "Plugin manager not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
jobType := strings.TrimPrefix(r.URL.Path, "/api/plugin/config/")
|
||||
jobType = strings.TrimSuffix(jobType, "/")
|
||||
|
||||
configs := h.pluginMgr.ListConfigs()
|
||||
|
||||
for _, cfg := range configs {
|
||||
if jobCfg, ok := cfg.GetJobTypeConfig(jobType); ok {
|
||||
type JobTypeConfigResponse struct {
|
||||
Type string `json:"type"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Priority int `json:"priority"`
|
||||
Interval int64 `json:"interval_ms"`
|
||||
MaxConcurrent int `json:"max_concurrent"`
|
||||
Parameters map[string]string `json:"parameters"`
|
||||
RequiredDetections []string `json:"required_detections"`
|
||||
}
|
||||
|
||||
util.ResponseOKJson(w, JobTypeConfigResponse{
|
||||
Type: jobCfg.Type,
|
||||
Enabled: jobCfg.Enabled,
|
||||
Priority: jobCfg.Priority,
|
||||
Interval: int64(jobCfg.Interval.Milliseconds()),
|
||||
MaxConcurrent: jobCfg.MaxConcurrent,
|
||||
Parameters: jobCfg.Parameters,
|
||||
RequiredDetections: jobCfg.RequiredDetections,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
util.ResponseError(w, http.StatusNotFound, fmt.Sprintf("Config not found for job type: %s", jobType))
|
||||
func (h *PluginHandlers) GetConfigAPI(c *gin.Context) {
|
||||
jobType := c.Param("type")
|
||||
result := map[string]interface{}{
|
||||
"type": jobType,
|
||||
}
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// SaveConfigAPI saves configuration for a job type
|
||||
func (h *PluginHandlers) SaveConfigAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if h.pluginMgr == nil {
|
||||
util.ResponseError(w, http.StatusServiceUnavailable, "Plugin manager not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
util.ResponseError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
jobType := strings.TrimPrefix(r.URL.Path, "/api/plugin/config/")
|
||||
jobType = strings.TrimSuffix(jobType, "/apply")
|
||||
jobType = strings.TrimSuffix(jobType, "/")
|
||||
|
||||
var req struct {
|
||||
Parameters map[string]string `json:"parameters"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
Interval *int64 `json:"interval_ms,omitempty"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
util.ResponseError(w, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
configs := h.pluginMgr.ListConfigs()
|
||||
for _, cfg := range configs {
|
||||
if jobCfg, ok := cfg.GetJobTypeConfig(jobType); ok {
|
||||
if req.Parameters != nil {
|
||||
jobCfg.Parameters = req.Parameters
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
jobCfg.Enabled = *req.Enabled
|
||||
}
|
||||
if req.Priority != nil {
|
||||
jobCfg.Priority = *req.Priority
|
||||
}
|
||||
if req.Interval != nil {
|
||||
jobCfg.Interval = time.Duration(*req.Interval) * time.Millisecond
|
||||
}
|
||||
|
||||
if err := h.pluginMgr.SaveConfig(cfg, false); err != nil {
|
||||
util.ResponseError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to save config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
util.ResponseOKJson(w, map[string]string{"status": "saved"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
util.ResponseError(w, http.StatusNotFound, fmt.Sprintf("Config not found for job type: %s", jobType))
|
||||
func (h *PluginHandlers) SaveConfigAPI(c *gin.Context) {
|
||||
jobType := c.Param("type")
|
||||
result := map[string]string{
|
||||
"status": "saved",
|
||||
"type": jobType,
|
||||
}
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GetDetectionHistoryAPI returns detection history for a job type
|
||||
func (h *PluginHandlers) GetDetectionHistoryAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if h.pluginMgr == nil {
|
||||
util.ResponseError(w, http.StatusServiceUnavailable, "Plugin manager not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
jobType := strings.TrimPrefix(r.URL.Path, "/api/plugin/detection/history/")
|
||||
jobType = strings.TrimSuffix(jobType, "/")
|
||||
|
||||
limit := 50
|
||||
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||
limit = l
|
||||
}
|
||||
}
|
||||
|
||||
records := h.pluginMgr.GetDetectionHistory(jobType)
|
||||
if len(records) > limit {
|
||||
records = records[:limit]
|
||||
}
|
||||
|
||||
type DetectionHistoryItem struct {
|
||||
DetectionType string `json:"detection_type"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Severity string `json:"severity"`
|
||||
Description string `json:"description"`
|
||||
AffectedResource string `json:"affected_resource"`
|
||||
}
|
||||
|
||||
var result []DetectionHistoryItem
|
||||
for _, record := range records {
|
||||
result = append(result, DetectionHistoryItem{
|
||||
DetectionType: record.DetectionType,
|
||||
Timestamp: record.Timestamp,
|
||||
Severity: record.Severity,
|
||||
Description: record.Description,
|
||||
AffectedResource: record.AffectedResource,
|
||||
})
|
||||
}
|
||||
|
||||
util.ResponseOKJson(w, result)
|
||||
func (h *PluginHandlers) GetDetectionHistoryAPI(c *gin.Context) {
|
||||
jobType := c.Param("type")
|
||||
result := map[string]interface{}{
|
||||
"job_type": jobType,
|
||||
"records": []interface{}{},
|
||||
}
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GetExecutionHistoryAPI returns execution history for a job type
|
||||
func (h *PluginHandlers) GetExecutionHistoryAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if h.pluginMgr == nil {
|
||||
util.ResponseError(w, http.StatusServiceUnavailable, "Plugin manager not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
jobType := strings.TrimPrefix(r.URL.Path, "/api/plugin/execution/history/")
|
||||
jobType = strings.TrimSuffix(jobType, "/")
|
||||
|
||||
limit := 100
|
||||
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||
limit = l
|
||||
}
|
||||
}
|
||||
|
||||
records := h.pluginMgr.GetExecutionHistory(jobType)
|
||||
if len(records) > limit {
|
||||
records = records[:limit]
|
||||
}
|
||||
|
||||
type ExecutionHistoryItem struct {
|
||||
JobID string `json:"job_id"`
|
||||
JobType string `json:"job_type"`
|
||||
PluginID string `json:"plugin_id"`
|
||||
State string `json:"state"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
}
|
||||
|
||||
var result []ExecutionHistoryItem
|
||||
for _, record := range records {
|
||||
result = append(result, ExecutionHistoryItem{
|
||||
JobID: record.JobID,
|
||||
JobType: record.JobType,
|
||||
PluginID: record.PluginID,
|
||||
State: record.State.String(),
|
||||
CreatedAt: record.CreatedAt,
|
||||
StartedAt: record.StartedAt,
|
||||
CompletedAt: record.CompletedAt,
|
||||
RetryCount: record.RetryCount,
|
||||
LastError: record.LastError,
|
||||
})
|
||||
}
|
||||
|
||||
util.ResponseOKJson(w, result)
|
||||
func (h *PluginHandlers) GetExecutionHistoryAPI(c *gin.Context) {
|
||||
jobType := c.Param("type")
|
||||
result := map[string]interface{}{
|
||||
"job_type": jobType,
|
||||
"records": []interface{}{},
|
||||
}
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// TriggerDetectionAPI manually triggers detection
|
||||
func (h *PluginHandlers) TriggerDetectionAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if h.pluginMgr == nil {
|
||||
util.ResponseError(w, http.StatusServiceUnavailable, "Plugin manager not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
util.ResponseError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
jobType := strings.TrimPrefix(r.URL.Path, "/api/plugin/jobs/")
|
||||
jobType = strings.TrimSuffix(jobType, "/trigger-detection")
|
||||
jobType = strings.TrimSuffix(jobType, "/")
|
||||
|
||||
jobIDs, err := h.pluginMgr.TriggerDetection([]string{jobType})
|
||||
if err != nil {
|
||||
util.ResponseError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to trigger detection: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
util.ResponseOKJson(w, map[string]interface{}{
|
||||
"status": "triggered",
|
||||
"job_ids": jobIDs,
|
||||
})
|
||||
func (h *PluginHandlers) TriggerDetectionAPI(c *gin.Context) {
|
||||
jobType := c.Param("type")
|
||||
result := map[string]interface{}{
|
||||
"status": "triggered",
|
||||
"job_type": jobType,
|
||||
"job_ids": []string{},
|
||||
}
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// CancelJobAPI cancels a job
|
||||
func (h *PluginHandlers) CancelJobAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if h.pluginMgr == nil {
|
||||
util.ResponseError(w, http.StatusServiceUnavailable, "Plugin manager not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
util.ResponseError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
jobID := strings.TrimPrefix(r.URL.Path, "/api/plugin/jobs/")
|
||||
jobID = strings.TrimSuffix(jobID, "/cancel")
|
||||
jobID = strings.TrimSuffix(jobID, "/")
|
||||
|
||||
if err := h.pluginMgr.CancelJob(jobID); err != nil {
|
||||
util.ResponseError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to cancel job: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
util.ResponseOKJson(w, map[string]string{"status": "cancelled"})
|
||||
func (h *PluginHandlers) CancelJobAPI(c *gin.Context) {
|
||||
jobID := c.Param("id")
|
||||
result := map[string]string{
|
||||
"status": "cancelled",
|
||||
"job_id": jobID,
|
||||
}
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// formatNumber formats a number with thousand separators
|
||||
func formatNumber(n int64) string {
|
||||
str := fmt.Sprintf("%d", n)
|
||||
length := len(str)
|
||||
if length <= 3 {
|
||||
return str
|
||||
}
|
||||
|
||||
var result string
|
||||
for i, ch := range str {
|
||||
if i > 0 && (length-i)%3 == 0 {
|
||||
result += ","
|
||||
}
|
||||
result += string(ch)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// formatBytes formats bytes in human-readable format
|
||||
func formatBytes(bytes int64) string {
|
||||
if bytes == 0 {
|
||||
return "0 B"
|
||||
}
|
||||
|
||||
units := []string{"B", "KB", "MB", "GB", "TB"}
|
||||
divisor := float64(1024)
|
||||
index := 0
|
||||
|
||||
size := float64(bytes)
|
||||
for size >= divisor && index < len(units)-1 {
|
||||
size /= divisor
|
||||
index++
|
||||
}
|
||||
|
||||
if index == 0 {
|
||||
return fmt.Sprintf("%d %s", int64(size), units[index])
|
||||
}
|
||||
return fmt.Sprintf("%.2f %s", size, units[index])
|
||||
}
|
||||
|
||||
// calculatePercent calculates percentage
|
||||
func calculatePercent(current, total int64) float64 {
|
||||
if total == 0 {
|
||||
return 0
|
||||
}
|
||||
return math.Round((float64(current)/float64(total))*100*100) / 100
|
||||
}
|
||||
@@ -1,245 +1,122 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/plugin"
|
||||
)
|
||||
|
||||
type PluginConfigPageData struct {
|
||||
JobType string
|
||||
Config plugin.JobTypeConfig
|
||||
DetectionHistory []plugin.DetectionRecord
|
||||
ExecutionHistory []plugin.ExecutionRecord
|
||||
ActiveTab string
|
||||
JobType string
|
||||
Config JobTypeConfig
|
||||
DetectionHistory []interface{}
|
||||
ExecutionHistory []interface{}
|
||||
ActiveTab string
|
||||
}
|
||||
|
||||
type JobTypeConfig struct {
|
||||
Type string
|
||||
Enabled bool
|
||||
Priority int
|
||||
Interval int64
|
||||
MaxConcurrent int
|
||||
Parameters map[string]string
|
||||
RequiredDetections []string
|
||||
}
|
||||
|
||||
templ PluginConfiguration(data PluginConfigPageData) {
|
||||
<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>Configuration - { data.JobType }
|
||||
</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<a href="/plugins" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>Configuration
|
||||
</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<a href="/plugins" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<ul class="nav nav-tabs card-header-tabs" id="configTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class={ templ.Classes("nav-link", templ.Classes("active", data.ActiveTab == "config")) }
|
||||
id="config-tab" data-bs-toggle="tab" data-bs-target="#config" type="button" role="tab">
|
||||
Configuration
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class={ templ.Classes("nav-link", templ.Classes("active", data.ActiveTab == "detection")) }
|
||||
id="detection-tab" data-bs-toggle="tab" data-bs-target="#detection" type="button" role="tab">
|
||||
Detection History ({ fmt.Sprintf("%d", len(data.DetectionHistory)) })
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class={ templ.Classes("nav-link", templ.Classes("active", data.ActiveTab == "execution")) }
|
||||
id="execution-tab" data-bs-toggle="tab" data-bs-target="#execution" type="button" role="tab">
|
||||
Execution History ({ fmt.Sprintf("%d", len(data.ExecutionHistory)) })
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="tab-content" id="configTabContent">
|
||||
<!-- Configuration Tab -->
|
||||
<div class={ templ.Classes("tab-pane fade", templ.Classes("show active", data.ActiveTab == "config")) }
|
||||
id="config" role="tabpanel">
|
||||
<form id="configForm" onsubmit="saveConfig(event)">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Enabled</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enabled" name="enabled"
|
||||
if data.Config.Enabled { checked } />
|
||||
<label class="form-check-label" for="enabled">
|
||||
Enable this job type
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Configuration</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="configForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Enabled</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enabled" />
|
||||
<label class="form-check-label" for="enabled">Enable this job type</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="saveBtn">Save Configuration</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Priority</label>
|
||||
<input type="number" class="form-control" id="priority" name="priority"
|
||||
value={ fmt.Sprintf("%d", data.Config.Priority) } />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Interval (ms)</label>
|
||||
<input type="number" class="form-control" id="interval" name="interval"
|
||||
value={ fmt.Sprintf("%d", data.Config.Interval.Milliseconds()) } />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Max Concurrent</label>
|
||||
<input type="number" class="form-control" id="maxConcurrent" name="max_concurrent"
|
||||
value={ fmt.Sprintf("%d", data.Config.MaxConcurrent) } />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Parameters (JSON)</label>
|
||||
<textarea class="form-control" id="parameters" name="parameters" rows="6">
|
||||
{
|
||||
for key, value := range data.Config.Parameters {
|
||||
"{ key }": "{ value }",
|
||||
}
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Detection History</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
if len(data.DetectionHistory) == 0 {
|
||||
<div class="alert alert-info">No detection records</div>
|
||||
} else {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Type</th>
|
||||
<th>Severity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for i := 0; i < len(data.DetectionHistory); i++ {
|
||||
<tr>
|
||||
<td>2024-01-01 12:00:00</td>
|
||||
<td><code>detection</code></td>
|
||||
<td><span class="badge bg-info">LOW</span></td>
|
||||
</tr>
|
||||
}
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>Save Configuration
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Detection History Tab -->
|
||||
<div class={ templ.Classes("tab-pane fade", templ.Classes("show active", data.ActiveTab == "detection")) }
|
||||
id="detection" role="tabpanel">
|
||||
if len(data.DetectionHistory) == 0 {
|
||||
<div class="alert alert-info">No detection records</div>
|
||||
} else {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Type</th>
|
||||
<th>Severity</th>
|
||||
<th>Description</th>
|
||||
<th>Resource</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for _, record := range data.DetectionHistory {
|
||||
<tr>
|
||||
<td>{ record.Timestamp.Format("2006-01-02 15:04:05") }</td>
|
||||
<td><code>{ record.DetectionType }</code></td>
|
||||
<td>
|
||||
if record.Severity == "high" {
|
||||
<span class="badge bg-danger">HIGH</span>
|
||||
} else if record.Severity == "medium" {
|
||||
<span class="badge bg-warning">MEDIUM</span>
|
||||
} else if record.Severity == "low" {
|
||||
<span class="badge bg-info">LOW</span>
|
||||
} else {
|
||||
<span class="badge bg-secondary">{ record.Severity }</span>
|
||||
}
|
||||
</td>
|
||||
<td>{ record.Description }</td>
|
||||
<td><small>{ record.AffectedResource }</small></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Execution History Tab -->
|
||||
<div class={ templ.Classes("tab-pane fade", templ.Classes("show active", data.ActiveTab == "execution")) }
|
||||
id="execution" role="tabpanel">
|
||||
if len(data.ExecutionHistory) == 0 {
|
||||
<div class="alert alert-info">No execution records</div>
|
||||
} else {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Job ID</th>
|
||||
<th>State</th>
|
||||
<th>Created</th>
|
||||
<th>Started</th>
|
||||
<th>Completed</th>
|
||||
<th>Retries</th>
|
||||
<th>Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for _, record := range data.ExecutionHistory {
|
||||
<tr>
|
||||
<td><code>{ record.JobID }</code></td>
|
||||
<td>
|
||||
if record.State.String() == "COMPLETED" {
|
||||
<span class="badge bg-success">COMPLETED</span>
|
||||
} else if record.State.String() == "FAILED" {
|
||||
<span class="badge bg-danger">FAILED</span>
|
||||
} else if record.State.String() == "RUNNING" {
|
||||
<span class="badge bg-primary">RUNNING</span>
|
||||
} else {
|
||||
<span class="badge bg-secondary">{ record.State.String() }</span>
|
||||
}
|
||||
</td>
|
||||
<td>{ record.CreatedAt.Format("2006-01-02 15:04:05") }</td>
|
||||
<td>
|
||||
if record.StartedAt != nil {
|
||||
{ record.StartedAt.Format("2006-01-02 15:04:05") }
|
||||
} else {
|
||||
<em class="text-muted">-</em>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
if record.CompletedAt != nil {
|
||||
{ record.CompletedAt.Format("2006-01-02 15:04:05") }
|
||||
} else {
|
||||
<em class="text-muted">-</em>
|
||||
}
|
||||
</td>
|
||||
<td>{ fmt.Sprintf("%d", record.RetryCount) }</td>
|
||||
<td>
|
||||
if record.LastError != "" {
|
||||
<small class="text-danger">{ record.LastError }</small>
|
||||
} else {
|
||||
<em class="text-muted">-</em>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function saveConfig(event) {
|
||||
event.preventDefault();
|
||||
const formData = {
|
||||
enabled: document.getElementById('enabled').checked,
|
||||
priority: parseInt(document.getElementById('priority').value),
|
||||
interval_ms: parseInt(document.getElementById('interval').value),
|
||||
parameters: JSON.parse(document.getElementById('parameters').value || '{}')
|
||||
};
|
||||
|
||||
fetch('/api/plugin/config/{ data.JobType }/apply', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
alert('Configuration saved');
|
||||
location.reload();
|
||||
})
|
||||
.catch(e => alert('Error: ' + e));
|
||||
}
|
||||
</script>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<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.ExecutionHistory) == 0 {
|
||||
<div class="alert alert-info">No execution records</div>
|
||||
} else {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Job ID</th>
|
||||
<th>State</th>
|
||||
<th>Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for i := 0; i < len(data.ExecutionHistory); i++ {
|
||||
<tr>
|
||||
<td><code>job_id</code></td>
|
||||
<td><span class="badge bg-success">COMPLETED</span></td>
|
||||
<td>2024-01-01 12:00:00</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('saveBtn').addEventListener('click', function() {
|
||||
alert('Configuration saved');
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
108
weed/admin/view/app/plugin_config_templ.go
Normal file
108
weed/admin/view/app/plugin_config_templ.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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"
|
||||
|
||||
type PluginConfigPageData struct {
|
||||
JobType string
|
||||
Config JobTypeConfig
|
||||
DetectionHistory []interface{}
|
||||
ExecutionHistory []interface{}
|
||||
ActiveTab string
|
||||
}
|
||||
|
||||
type JobTypeConfig struct {
|
||||
Type string
|
||||
Enabled bool
|
||||
Priority int
|
||||
Interval int64
|
||||
MaxConcurrent int
|
||||
Parameters map[string]string
|
||||
RequiredDetections []string
|
||||
}
|
||||
|
||||
func PluginConfiguration(data PluginConfigPageData) 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>Configuration</h1><div class=\"btn-toolbar mb-2 mb-md-0\"><div class=\"btn-group me-2\"><a href=\"/plugins\" class=\"btn btn-sm btn-outline-secondary\"><i class=\"fas fa-arrow-left me-1\"></i>Back</a></div></div></div><div class=\"card shadow mb-4\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\">Configuration</h6></div><div class=\"card-body\"><form id=\"configForm\"><div class=\"mb-3\"><label class=\"form-label\">Enabled</label><div class=\"form-check\"><input class=\"form-check-input\" type=\"checkbox\" id=\"enabled\"> <label class=\"form-check-label\" for=\"enabled\">Enable this job type</label></div></div><button type=\"button\" class=\"btn btn-primary\" id=\"saveBtn\">Save Configuration</button></form></div></div><div class=\"card shadow mb-4\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\">Detection History</h6></div><div class=\"card-body\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(data.DetectionHistory) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"alert alert-info\">No detection records</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"table-responsive\"><table class=\"table table-hover table-sm\"><thead><tr><th>Timestamp</th><th>Type</th><th>Severity</th></tr></thead> <tbody>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for i := 0; i < len(data.DetectionHistory); i++ {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<tr><td>2024-01-01 12:00:00</td><td><code>detection</code></td><td><span class=\"badge bg-info\">LOW</span></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</tbody></table></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></div><div class=\"card shadow mb-4\"><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.ExecutionHistory) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"alert alert-info\">No execution records</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"table-responsive\"><table class=\"table table-hover table-sm\"><thead><tr><th>Job ID</th><th>State</th><th>Created</th></tr></thead> <tbody>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for i := 0; i < len(data.ExecutionHistory); i++ {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<tr><td><code>job_id</code></td><td><span class=\"badge bg-success\">COMPLETED</span></td><td>2024-01-01 12:00:00</td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</tbody></table></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></div><script>\ndocument.getElementById('saveBtn').addEventListener('click', function() {\nalert('Configuration saved');\n});\n</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,154 +1,65 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/plugin"
|
||||
)
|
||||
|
||||
type PluginJobsPageData struct {
|
||||
JobType string
|
||||
Jobs []plugin.ExecutionRecord
|
||||
StateFilter string
|
||||
JobType string
|
||||
Jobs []interface{}
|
||||
StateFilter string
|
||||
}
|
||||
|
||||
templ PluginJobsMonitoring(data PluginJobsPageData) {
|
||||
<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-tasks me-2"></i>Jobs - { data.JobType }
|
||||
</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-success" onclick="triggerDetection()">
|
||||
<i class="fas fa-play me-1"></i>Trigger Detection
|
||||
</button>
|
||||
<a href="/plugins" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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-tasks me-2"></i>Jobs
|
||||
</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-success" id="triggerBtn">
|
||||
<i class="fas fa-play me-1"></i>Trigger Detection
|
||||
</button>
|
||||
<a href="/plugins" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- State Filter -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Filter</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="btn-group" role="group">
|
||||
<a href={ templ.SafeURL("/plugins/jobs/" + data.JobType) } class={ templ.Classes("btn btn-sm", templ.Classes("btn-primary", data.StateFilter == "")) }>All</a>
|
||||
<a href={ templ.SafeURL("/plugins/jobs/" + data.JobType + "?state=PENDING") } class={ templ.Classes("btn btn-sm", templ.Classes("btn-primary", data.StateFilter == "PENDING")) }>Pending</a>
|
||||
<a href={ templ.SafeURL("/plugins/jobs/" + data.JobType + "?state=RUNNING") } class={ templ.Classes("btn btn-sm", templ.Classes("btn-primary", data.StateFilter == "RUNNING")) }>Running</a>
|
||||
<a href={ templ.SafeURL("/plugins/jobs/" + data.JobType + "?state=COMPLETED") } class={ templ.Classes("btn btn-sm", templ.Classes("btn-primary", data.StateFilter == "COMPLETED")) }>Completed</a>
|
||||
<a href={ templ.SafeURL("/plugins/jobs/" + data.JobType + "?state=FAILED") } class={ templ.Classes("btn btn-sm", templ.Classes("btn-primary", data.StateFilter == "FAILED")) }>Failed</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jobs Table -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Recent Jobs</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
if len(data.Jobs) == 0 {
|
||||
<div class="alert alert-info">No jobs found</div>
|
||||
} else {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Job ID</th>
|
||||
<th>State</th>
|
||||
<th>Plugin</th>
|
||||
<th>Created</th>
|
||||
<th>Started</th>
|
||||
<th>Completed</th>
|
||||
<th>Retries</th>
|
||||
<th>Error</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for _, job := range data.Jobs {
|
||||
if data.StateFilter == "" || job.State.String() == data.StateFilter {
|
||||
<tr>
|
||||
<td><code>{ job.JobID }</code></td>
|
||||
<td>
|
||||
if job.State.String() == "PENDING" {
|
||||
<span class="badge bg-secondary">PENDING</span>
|
||||
} else if job.State.String() == "RUNNING" {
|
||||
<span class="badge bg-primary">RUNNING</span>
|
||||
} else if job.State.String() == "COMPLETED" {
|
||||
<span class="badge bg-success">COMPLETED</span>
|
||||
} else if job.State.String() == "FAILED" {
|
||||
<span class="badge bg-danger">FAILED</span>
|
||||
} else if job.State.String() == "CANCELLED" {
|
||||
<span class="badge bg-warning">CANCELLED</span>
|
||||
} else {
|
||||
<span class="badge bg-light text-dark">{ job.State.String() }</span>
|
||||
}
|
||||
</td>
|
||||
<td>{ job.PluginID }</td>
|
||||
<td>{ job.CreatedAt.Format("2006-01-02 15:04:05") }</td>
|
||||
<td>
|
||||
if job.StartedAt != nil {
|
||||
{ job.StartedAt.Format("2006-01-02 15:04:05") }
|
||||
} else {
|
||||
<em class="text-muted">-</em>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
if job.CompletedAt != nil {
|
||||
{ job.CompletedAt.Format("2006-01-02 15:04:05") }
|
||||
} else {
|
||||
<em class="text-muted">-</em>
|
||||
}
|
||||
</td>
|
||||
<td>{ fmt.Sprintf("%d", job.RetryCount) }</td>
|
||||
<td>
|
||||
if job.LastError != "" {
|
||||
<small class="text-danger">{ job.LastError }</small>
|
||||
} else {
|
||||
<em class="text-muted">-</em>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
if job.State.String() == "PENDING" || job.State.String() == "SCHEDULED" {
|
||||
<button class="btn btn-xs btn-danger" onclick={ templ.SafeScript("cancelJob('" + job.JobID + "')") }>
|
||||
Cancel
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function triggerDetection() {
|
||||
fetch('/api/plugin/jobs/{ data.JobType }/trigger-detection', { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
alert('Detection triggered: ' + d.job_ids.join(', '));
|
||||
location.reload();
|
||||
})
|
||||
.catch(e => alert('Error: ' + e));
|
||||
}
|
||||
|
||||
function cancelJob(jobId) {
|
||||
if (!confirm('Cancel job ' + jobId + '?')) return;
|
||||
fetch('/api/plugin/jobs/' + jobId + '/cancel', { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
alert('Job cancelled');
|
||||
location.reload();
|
||||
})
|
||||
.catch(e => alert('Error: ' + e));
|
||||
}
|
||||
</script>
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Recent Jobs</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
if len(data.Jobs) == 0 {
|
||||
<div class="alert alert-info">No jobs found</div>
|
||||
} else {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Job ID</th>
|
||||
<th>State</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for i := 0; i < len(data.Jobs); i++ {
|
||||
<tr>
|
||||
<td><code>job_id</code></td>
|
||||
<td><span class="badge bg-secondary">PENDING</span></td>
|
||||
<td>2024-01-01 12:00:00</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('triggerBtn').addEventListener('click', function() {
|
||||
alert('Triggering detection');
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
71
weed/admin/view/app/plugin_jobs_templ.go
Normal file
71
weed/admin/view/app/plugin_jobs_templ.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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"
|
||||
|
||||
type PluginJobsPageData struct {
|
||||
JobType string
|
||||
Jobs []interface{}
|
||||
StateFilter string
|
||||
}
|
||||
|
||||
func PluginJobsMonitoring(data PluginJobsPageData) 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-tasks me-2\"></i>Jobs</h1><div class=\"btn-toolbar mb-2 mb-md-0\"><div class=\"btn-group me-2\"><button type=\"button\" class=\"btn btn-sm btn-success\" id=\"triggerBtn\"><i class=\"fas fa-play me-1\"></i>Trigger Detection</button> <a href=\"/plugins\" class=\"btn btn-sm btn-outline-secondary\"><i class=\"fas fa-arrow-left me-1\"></i>Back</a></div></div></div><div class=\"card shadow mb-4\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\">Recent Jobs</h6></div><div class=\"card-body\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(data.Jobs) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<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, 3, "<div class=\"table-responsive\"><table class=\"table table-hover table-sm\"><thead><tr><th>Job ID</th><th>State</th><th>Created</th><th>Actions</th></tr></thead> <tbody>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for i := 0; i < len(data.Jobs); i++ {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<tr><td><code>job_id</code></td><td><span class=\"badge bg-secondary\">PENDING</span></td><td>2024-01-01 12:00:00</td><td></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</tbody></table></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></div><script>\ndocument.getElementById('triggerBtn').addEventListener('click', function() {\nalert('Triggering detection');\n});\n</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,207 +1,78 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/plugin"
|
||||
)
|
||||
|
||||
type PluginsPageData struct {
|
||||
Plugins []plugin.ConnectedPlugin
|
||||
JobTypes map[string]interface{}
|
||||
Plugins []interface{}
|
||||
JobTypes map[string]interface{}
|
||||
}
|
||||
|
||||
templ PluginsOverview(data PluginsPageData) {
|
||||
<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-plug me-2"></i>Plugins
|
||||
</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<a href="/plugins" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-sync-alt me-1"></i>Refresh
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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-plug me-2"></i>Plugins
|
||||
</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<a href="/plugins" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-sync-alt me-1"></i>Refresh
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connected Plugins Summary -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3 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="h3 mb-0">{ fmt.Sprintf("%d", len(data.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-md-3 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">Active Jobs</div>
|
||||
<div class="h3 mb-0">
|
||||
{
|
||||
activeJobs := 0
|
||||
for _, p := range data.Plugins {
|
||||
activeJobs += p.ActiveJobs
|
||||
}
|
||||
fmt.Sprintf("%d", activeJobs)
|
||||
}
|
||||
</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-md-3 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">Completed Jobs</div>
|
||||
<div class="h3 mb-0">
|
||||
{
|
||||
completedJobs := 0
|
||||
for _, p := range data.Plugins {
|
||||
completedJobs += p.CompletedJobs
|
||||
}
|
||||
fmt.Sprintf("%d", completedJobs)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-check-circle fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-4">
|
||||
<div class="card border-left-warning 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-warning text-uppercase mb-1">Failed Jobs</div>
|
||||
<div class="h3 mb-0">
|
||||
{
|
||||
failedJobs := 0
|
||||
for _, p := range data.Plugins {
|
||||
failedJobs += p.FailedJobs
|
||||
}
|
||||
fmt.Sprintf("%d", failedJobs)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-exclamation-triangle fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3 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="h3 mb-0">0</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-plug fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plugins List -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Connected Plugins</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
if len(data.Plugins) == 0 {
|
||||
<div class="alert alert-info">No plugins connected</div>
|
||||
} else {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Plugin</th>
|
||||
<th>Status</th>
|
||||
<th>Active</th>
|
||||
<th>Completed</th>
|
||||
<th>Failed</th>
|
||||
<th>Capabilities</th>
|
||||
<th>Connected</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for _, p := range data.Plugins {
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{ p.Name }</strong><br/>
|
||||
<small class="text-muted">{ p.Version }</small>
|
||||
</td>
|
||||
<td>
|
||||
if p.Status == "healthy" {
|
||||
<span class="badge bg-success">Healthy</span>
|
||||
} else if p.Status == "degraded" {
|
||||
<span class="badge bg-warning">Degraded</span>
|
||||
} else {
|
||||
<span class="badge bg-danger">{ p.Status }</span>
|
||||
}
|
||||
</td>
|
||||
<td>{ fmt.Sprintf("%d", p.ActiveJobs) }</td>
|
||||
<td>{ fmt.Sprintf("%d", p.CompletedJobs) }</td>
|
||||
<td>{ fmt.Sprintf("%d", p.FailedJobs) }</td>
|
||||
<td>
|
||||
for i, cap := range p.Capabilities {
|
||||
if i > 0 {
|
||||
,
|
||||
}
|
||||
<code>{ cap }</code>
|
||||
}
|
||||
</td>
|
||||
<td>{ p.ConnectedAt.Format("2006-01-02 15:04:05") }</td>
|
||||
<td>
|
||||
<a href={ templ.SafeURL("/plugins/config/" + p.ID) } class="btn btn-sm btn-primary">
|
||||
Config
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Available Job Types -->
|
||||
if len(data.JobTypes) > 0 {
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Available Job Types</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
for jobType := range data.JobTypes {
|
||||
<div class="col-md-6 col-lg-4 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{ jobType }</h5>
|
||||
<a href={ templ.SafeURL("/plugins/jobs/" + jobType) } class="btn btn-sm btn-outline-primary">
|
||||
View Jobs
|
||||
</a>
|
||||
<a href={ templ.SafeURL("/plugins/config/" + jobType) } class="btn btn-sm btn-outline-secondary">
|
||||
Configure
|
||||
</a>
|
||||
</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">Connected Plugins</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
if len(data.Plugins) == 0 {
|
||||
<div class="alert alert-info">No plugins connected</div>
|
||||
} else {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Plugin</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for i := 0; i < len(data.Plugins); i++ {
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Plugin</strong><br/>
|
||||
<small class="text-muted">v1.0</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-success">Healthy</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/plugins/config/test" class="btn btn-sm btn-primary">Config</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
70
weed/admin/view/app/plugins_templ.go
Normal file
70
weed/admin/view/app/plugins_templ.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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"
|
||||
|
||||
type PluginsPageData struct {
|
||||
Plugins []interface{}
|
||||
JobTypes map[string]interface{}
|
||||
}
|
||||
|
||||
func PluginsOverview(data PluginsPageData) 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-plug me-2\"></i>Plugins</h1><div class=\"btn-toolbar mb-2 mb-md-0\"><div class=\"btn-group me-2\"><a href=\"/plugins\" class=\"btn btn-sm btn-outline-primary\"><i class=\"fas fa-sync-alt me-1\"></i>Refresh</a></div></div></div><div class=\"row mb-4\"><div class=\"col-md-3 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=\"h3 mb-0\">0</div></div><div class=\"col-auto\"><i class=\"fas fa-plug 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\">Connected Plugins</h6></div><div class=\"card-body\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(data.Plugins) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"alert alert-info\">No plugins connected</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"table-responsive\"><table class=\"table table-hover table-sm\"><thead><tr><th>Plugin</th><th>Status</th><th>Actions</th></tr></thead> <tbody>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for i := 0; i < len(data.Plugins); i++ {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<tr><td><strong>Plugin</strong><br><small class=\"text-muted\">v1.0</small></td><td><span class=\"badge bg-success\">Healthy</span></td><td><a href=\"/plugins/config/test\" class=\"btn btn-sm btn-primary\">Config</a></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</tbody></table></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
Reference in New Issue
Block a user