feat(plugin): Register plugin routes and API handlers

This commit is contained in:
Chris Lu committed 2026-02-17 03:19:52 -08:00
1 parent d387be45aa
commit 1dbaca70f8
9 files changed
+628 -970

No files matched your search

-54
View File
@@ -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
}
+110 -233
View File
@@ -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>
</tbody>
</table>
</div>
}
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Save Configuration
</button>
</form>
</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>
<!-- 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>
<script>
document.getElementById('saveBtn').addEventListener('click', function() {
alert('Configuration saved');
});
</script>
}
+108
View 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
+56 -145
View File
@@ -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>
<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>
<!-- 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>
<script>
document.getElementById('triggerBtn').addEventListener('click', function() {
alert('Triggering detection');
});
</script>
}
+71
View 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
+69 -198
View File
@@ -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 {
,&nbsp;
}
<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
View 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