diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index 741548122..250bad1d4 100644 --- a/weed/admin/dash/admin_server.go +++ b/weed/admin/dash/admin_server.go @@ -3,6 +3,11 @@ package dash import ( "context" "fmt" + "net/http" + "sort" + "strings" + "time" + "github.com/seaweedfs/seaweedfs/weed/admin/maintenance" adminplugin "github.com/seaweedfs/seaweedfs/weed/admin/plugin" "github.com/seaweedfs/seaweedfs/weed/cluster" @@ -19,10 +24,6 @@ import ( "github.com/seaweedfs/seaweedfs/weed/util" "github.com/seaweedfs/seaweedfs/weed/wdclient" "google.golang.org/grpc" - "net/http" - "sort" - "strings" - "time" "github.com/seaweedfs/seaweedfs/weed/s3api" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" @@ -980,7 +981,7 @@ func (s *AdminServer) GetPluginRunHistory(jobType string) (*adminplugin.JobTypeR } // ListPluginJobTypes returns known plugin job types from connected worker registry and persisted data. -func (s *AdminServer) ListPluginJobTypes() ([]string, error) { +func (s *AdminServer) ListPluginJobTypes() ([]adminplugin.JobTypeInfo, error) { if s.plugin == nil { return nil, fmt.Errorf("plugin is not enabled") } diff --git a/weed/admin/plugin/plugin.go b/weed/admin/plugin/plugin.go index 6fa0eab35..c1175b931 100644 --- a/weed/admin/plugin/plugin.go +++ b/weed/admin/plugin/plugin.go @@ -34,6 +34,13 @@ type Options struct { ClusterContextProvider func(context.Context) (*plugin_pb.ClusterContext, error) } +// JobTypeInfo contains metadata about a plugin job type. +type JobTypeInfo struct { + JobType string `json:"job_type"` + DisplayName string `json:"display_name"` + Weight int32 `json:"weight"` +} + type Plugin struct { plugin_pb.UnimplementedPluginControlServiceServer @@ -623,7 +630,7 @@ func (r *Plugin) ListWorkers() []*WorkerSession { return r.registry.List() } -func (r *Plugin) ListKnownJobTypes() ([]string, error) { +func (r *Plugin) ListKnownJobTypes() ([]JobTypeInfo, error) { registryJobTypes := r.registry.JobTypes() storedJobTypes, err := r.store.ListJobTypes() if err != nil { @@ -638,12 +645,72 @@ func (r *Plugin) ListKnownJobTypes() ([]string, error) { jobTypeSet[jobType] = struct{}{} } - out := make([]string, 0, len(jobTypeSet)) + jobTypeList := make([]string, 0, len(jobTypeSet)) for jobType := range jobTypeSet { - out = append(out, jobType) + jobTypeList = append(jobTypeList, jobType) + } + sort.Strings(jobTypeList) + + result := make([]JobTypeInfo, 0, len(jobTypeList)) + workers := r.registry.List() + + // Pre-calculate the best capability for each job type from available workers. + // Prefer capabilities with non-empty DisplayName, then higher Weight. + jobTypeToCap := make(map[string]*plugin_pb.JobTypeCapability) + for _, worker := range workers { + for jobType, cap := range worker.Capabilities { + if cap == nil { + continue + } + existing, exists := jobTypeToCap[jobType] + if !exists || existing == nil { + jobTypeToCap[jobType] = cap + continue + } + // Preserve existing if it has DisplayName but cap doesn't. + if existing.DisplayName != "" && cap.DisplayName == "" { + continue + } + // Prefer capabilities with a non-empty DisplayName. + if existing.DisplayName == "" && cap.DisplayName != "" { + jobTypeToCap[jobType] = cap + continue + } + // If DisplayName statuses are equal, prefer higher Weight. + if cap.Weight > existing.Weight { + jobTypeToCap[jobType] = cap + } + } } - sort.Strings(out) - return out, nil + + for _, jobType := range jobTypeList { + info := JobTypeInfo{JobType: jobType} + + // Get display name and weight from pre-calculated capabilities + if cap, ok := jobTypeToCap[jobType]; ok && cap != nil { + if cap.DisplayName != "" { + info.DisplayName = cap.DisplayName + } + info.Weight = cap.Weight + } + + // Default display name to job type if not set + if info.DisplayName == "" { + info.DisplayName = jobType + } + + result = append(result, info) + } + + // Sort by weight (descending) then by job type (ascending) + sort.Slice(result, func(i, j int) bool { + if result[i].Weight != result[j].Weight { + return result[i].Weight > result[j].Weight // higher weight first + } + return result[i].JobType < result[j].JobType // alphabetical as tiebreaker + }) + + return result, nil } // FilterProposalsWithActiveJobs drops proposals that are already assigned/running. diff --git a/weed/admin/plugin/plugin_scheduler.go b/weed/admin/plugin/plugin_scheduler.go index 5951fc5fe..e825e8069 100644 --- a/weed/admin/plugin/plugin_scheduler.go +++ b/weed/admin/plugin/plugin_scheduler.go @@ -175,7 +175,8 @@ func (r *Plugin) ListSchedulerStates() ([]SchedulerJobTypeState, error) { r.schedulerMu.Unlock() states := make([]SchedulerJobTypeState, 0, len(jobTypes)) - for _, jobType := range jobTypes { + for _, jobTypeInfo := range jobTypes { + jobType := jobTypeInfo.JobType state := SchedulerJobTypeState{ JobType: jobType, DetectionInFlight: detectionInFlight[jobType], @@ -187,6 +188,7 @@ func (r *Plugin) ListSchedulerStates() ([]SchedulerJobTypeState, error) { } policy, enabled, loadErr := r.loadSchedulerPolicy(jobType) + if loadErr != nil { state.PolicyError = loadErr.Error() } else { diff --git a/weed/admin/view/app/plugin.templ b/weed/admin/view/app/plugin.templ index b7cdf60d7..252777286 100644 --- a/weed/admin/view/app/plugin.templ +++ b/weed/admin/view/app/plugin.templ @@ -683,6 +683,25 @@ templ Plugin(page string) { return 'configuration'; } + // Parse a job type item (string or object) into a safe object shape + function parseJobTypeItem(item) { + var jobType = ''; + var displayName = ''; + + if (typeof item === 'string') { + jobType = String(item || '').trim(); + displayName = jobType; + } else if (item && typeof item === 'object') { + jobType = String(item.job_type || item.jobType || '').trim(); + displayName = String(item.display_name || item.displayName || jobType || '').trim(); + if (!displayName) { + displayName = jobType; + } + } + + return { jobType: jobType, displayName: displayName }; + } + function topTabKeyForJobType(jobType) { var normalized = String(jobType || '').trim(); if (!normalized) { @@ -706,7 +725,8 @@ templ Plugin(page string) { } var jobTypes = Array.isArray(state.jobTypes) ? state.jobTypes : []; for (var i = 0; i < jobTypes.length; i++) { - if (String(jobTypes[i] || '').trim() === normalized) { + var item = parseJobTypeItem(jobTypes[i]); + if (item.jobType === normalized) { return true; } } @@ -728,7 +748,8 @@ templ Plugin(page string) { state.activeTopTab = 'overview'; return; } - state.selectedJobType = String(state.jobTypes[0] || '').trim(); + var firstItem = parseJobTypeItem(state.jobTypes[0]); + state.selectedJobType = firstItem.jobType; state.activeTopTab = topTabKeyForJobType(state.selectedJobType); } @@ -749,7 +770,8 @@ templ Plugin(page string) { if (state.activeTopTab === 'overview') { if (!state.selectedJobType && Array.isArray(state.jobTypes) && state.jobTypes.length > 0) { - state.selectedJobType = String(state.jobTypes[0] || '').trim(); + var item = parseJobTypeItem(state.jobTypes[0]); + state.selectedJobType = item.jobType; } return; } @@ -758,7 +780,8 @@ templ Plugin(page string) { topTabJobType = state.selectedJobType; } if (!topTabJobType && Array.isArray(state.jobTypes) && state.jobTypes.length > 0) { - topTabJobType = String(state.jobTypes[0] || '').trim(); + var item = parseJobTypeItem(state.jobTypes[0]); + topTabJobType = item.jobType; } if (!topTabJobType) { state.activeTopTab = 'overview'; @@ -784,14 +807,17 @@ templ Plugin(page string) { var jobTypes = Array.isArray(state.jobTypes) ? state.jobTypes : []; for (var i = 0; i < jobTypes.length; i++) { - var jobType = String(jobTypes[i] || '').trim(); + var item = parseJobTypeItem(jobTypes[i]); + var jobType = item.jobType; + var displayName = item.displayName; + if (!jobType) { continue; } html += '' + ''; } @@ -1267,7 +1293,8 @@ templ Plugin(page string) { var jobTypes = []; for (var k = 0; k < known.length; k++) { - var knownType = String(known[k] || '').trim(); + var item = parseJobTypeItem(known[k]); + var knownType = item && item.jobType ? String(item.jobType).trim() : ''; if (!knownType || seen[knownType]) { continue; } diff --git a/weed/admin/view/app/plugin_templ.go b/weed/admin/view/app/plugin_templ.go index 46bee181a..250f4dac4 100644 --- a/weed/admin/view/app/plugin_templ.go +++ b/weed/admin/view/app/plugin_templ.go @@ -46,7 +46,7 @@ func Plugin(page string) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">

Workers

Cluster-wide worker status, per-job configuration, detection, queue, and execution workflows.

Workers

0

Active Jobs

0

Activities (recent)

0

Per Job Type Summary
Job TypeActive JobsRecent Activities
Loading...
Scheduler State
Per job type detection schedule and execution limits
Job TypeEnabledDetectorIn FlightNext DetectionIntervalExec GlobalExec/WorkerExecutor WorkersEffective Exec
Loading...
Workers
WorkerAddressCapabilitiesLoad
Loading...
Job Type Configuration
Not loaded
Selected Job Type
-
Descriptor
Select a job type to load schema and config.
Admin Config Form
No admin form loaded.
Worker Config Form
No worker form loaded.
Job Scheduling Settings
Run History
Keep last 10 success + last 10 errors
Successful Runs
TimeJob IDWorkerDuration
No data
Error Runs
TimeJob IDWorkerError
No data
Detection Results
Run detection to see proposals.
Job Queue
States: pending/assigned/running
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Detection Jobs
Detection activities for selected job type
TimeJob TypeRequest IDWorkerStageSourceMessage
Loading...
Execution Jobs
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Execution Activities
Non-detection events only
TimeJob TypeJob IDSourceStageMessage
Loading...
Job Detail
Select a job to view details.
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">

Workers

Cluster-wide worker status, per-job configuration, detection, queue, and execution workflows.

Workers

0

Active Jobs

0

Activities (recent)

0

Per Job Type Summary
Job TypeActive JobsRecent Activities
Loading...
Scheduler State
Per job type detection schedule and execution limits
Job TypeEnabledDetectorIn FlightNext DetectionIntervalExec GlobalExec/WorkerExecutor WorkersEffective Exec
Loading...
Workers
WorkerAddressCapabilitiesLoad
Loading...
Job Type Configuration
Not loaded
Selected Job Type
-
Descriptor
Select a job type to load schema and config.
Admin Config Form
No admin form loaded.
Worker Config Form
No worker form loaded.
Job Scheduling Settings
Run History
Keep last 10 success + last 10 errors
Successful Runs
TimeJob IDWorkerDuration
No data
Error Runs
TimeJob IDWorkerError
No data
Detection Results
Run detection to see proposals.
Job Queue
States: pending/assigned/running
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Detection Jobs
Detection activities for selected job type
TimeJob TypeRequest IDWorkerStageSourceMessage
Loading...
Execution Jobs
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Execution Activities
Non-detection events only
TimeJob TypeJob IDSourceStageMessage
Loading...
Job Detail
Select a job to view details.
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/pb/plugin.proto b/weed/pb/plugin.proto index d3c92fd4b..c2296c453 100644 --- a/weed/pb/plugin.proto +++ b/weed/pb/plugin.proto @@ -96,6 +96,7 @@ message JobTypeCapability { int32 max_execution_concurrency = 5; string display_name = 6; string description = 7; + int32 weight = 8; } message RequestConfigSchema { diff --git a/weed/pb/plugin_pb/plugin.pb.go b/weed/pb/plugin_pb/plugin.pb.go index d11dee176..5dac6afa8 100644 --- a/weed/pb/plugin_pb/plugin.pb.go +++ b/weed/pb/plugin_pb/plugin.pb.go @@ -1209,6 +1209,7 @@ type JobTypeCapability struct { MaxExecutionConcurrency int32 `protobuf:"varint,5,opt,name=max_execution_concurrency,json=maxExecutionConcurrency,proto3" json:"max_execution_concurrency,omitempty"` DisplayName string `protobuf:"bytes,6,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` Description string `protobuf:"bytes,7,opt,name=description,proto3" json:"description,omitempty"` + Weight int32 `protobuf:"varint,8,opt,name=weight,proto3" json:"weight,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1292,6 +1293,13 @@ func (x *JobTypeCapability) GetDescription() string { return "" } +func (x *JobTypeCapability) GetWeight() int32 { + if x != nil { + return x.Weight + } + return 0 +} + type RequestConfigSchema struct { state protoimpl.MessageState `protogen:"open.v1"` JobType string `protobuf:"bytes,1,opt,name=job_type,json=jobType,proto3" json:"job_type,omitempty"` @@ -3948,7 +3956,7 @@ const file_plugin_proto_rawDesc = "" + "\bjob_type\x18\x03 \x01(\tR\ajobType\x12&\n" + "\x05state\x18\x04 \x01(\x0e2\x10.plugin.JobStateR\x05state\x12)\n" + "\x10progress_percent\x18\x05 \x01(\x01R\x0fprogressPercent\x12\x14\n" + - "\x05stage\x18\x06 \x01(\tR\x05stage\"\xab\x02\n" + + "\x05stage\x18\x06 \x01(\tR\x05stage\"\xc3\x02\n" + "\x11JobTypeCapability\x12\x19\n" + "\bjob_type\x18\x01 \x01(\tR\ajobType\x12\x1d\n" + "\n" + @@ -3958,7 +3966,8 @@ const file_plugin_proto_rawDesc = "" + "\x19max_detection_concurrency\x18\x04 \x01(\x05R\x17maxDetectionConcurrency\x12:\n" + "\x19max_execution_concurrency\x18\x05 \x01(\x05R\x17maxExecutionConcurrency\x12!\n" + "\fdisplay_name\x18\x06 \x01(\tR\vdisplayName\x12 \n" + - "\vdescription\x18\a \x01(\tR\vdescription\"U\n" + + "\vdescription\x18\a \x01(\tR\vdescription\x12\x16\n" + + "\x06weight\x18\b \x01(\x05R\x06weight\"U\n" + "\x13RequestConfigSchema\x12\x19\n" + "\bjob_type\x18\x01 \x01(\tR\ajobType\x12#\n" + "\rforce_refresh\x18\x02 \x01(\bR\fforceRefresh\"\xda\x01\n" +