Browse Source

Add customizable plugin display names and weights (#8459)

* feat: add customizable plugin display names and weights

- Add weight field to JobTypeCapability proto message
- Modify ListKnownJobTypes() to return JobTypeInfo with display names and weights
- Modify ListPluginJobTypes() to return JobTypeInfo instead of string
- Sort plugins by weight (descending) then alphabetically
- Update admin API to return enriched job type metadata
- Update plugin UI template to display names instead of IDs
- Consolidate API by reusing existing function names instead of suffixed variants

* perf: optimize plugin job type capability lookup and add null-safe parsing

- Pre-calculate job type capabilities in a map to reduce O(n*m) nested loops
  to O(n+m) lookup time in ListKnownJobTypes()
- Add parseJobTypeItem() helper function for null-safe job type item parsing
- Refactor plugin.templ to use parseJobTypeItem() in all job type access points
  (hasJobType, applyInitialNavigation, ensureActiveNavigation, renderTopTabs)
- Deterministic capability resolution by using first worker's capability

* templ

* refactor: use parseJobTypeItem helper consistently in plugin.templ

Replace duplicated job type extraction logic at line 1296-1298 with
parseJobTypeItem() helper function for consistency and maintainability.

* improve: prefer richer capability metadata and add null-safety checks

- Improve capability selection in ListKnownJobTypes() to prefer capabilities
  with non-empty DisplayName and higher Weight across all workers instead of
  first-wins approach. Handles mixed-version clusters better.
- Add defensive null checks in renderJobTypeSummary() to safely access
  parseJobTypeItem() result before property access
- Ensures malformed or missing entries won't break the rendering pipeline

* fix: preserve existing DisplayName when merging capabilities

Fix capability merge logic to respect existing DisplayName values:
- If existing has DisplayName but candidate doesn't, preserve existing
- If existing doesn't have DisplayName but candidate does, use candidate
- Only use Weight comparison if DisplayName status is equal
- Prevents higher-weight capabilities with empty DisplayName from
  overriding capabilities with non-empty DisplayName
pull/8360/head
Chris Lu 1 day ago
committed by GitHub
parent
commit
c73e65ad5e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 11
      weed/admin/dash/admin_server.go
  2. 77
      weed/admin/plugin/plugin.go
  3. 4
      weed/admin/plugin/plugin_scheduler.go
  4. 41
      weed/admin/view/app/plugin.templ
  5. 2
      weed/admin/view/app/plugin_templ.go
  6. 1
      weed/pb/plugin.proto
  7. 13
      weed/pb/plugin_pb/plugin.pb.go

11
weed/admin/dash/admin_server.go

@ -3,6 +3,11 @@ package dash
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"sort"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/admin/maintenance" "github.com/seaweedfs/seaweedfs/weed/admin/maintenance"
adminplugin "github.com/seaweedfs/seaweedfs/weed/admin/plugin" adminplugin "github.com/seaweedfs/seaweedfs/weed/admin/plugin"
"github.com/seaweedfs/seaweedfs/weed/cluster" "github.com/seaweedfs/seaweedfs/weed/cluster"
@ -19,10 +24,6 @@ import (
"github.com/seaweedfs/seaweedfs/weed/util" "github.com/seaweedfs/seaweedfs/weed/util"
"github.com/seaweedfs/seaweedfs/weed/wdclient" "github.com/seaweedfs/seaweedfs/weed/wdclient"
"google.golang.org/grpc" "google.golang.org/grpc"
"net/http"
"sort"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/s3api" "github.com/seaweedfs/seaweedfs/weed/s3api"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "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. // 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 { if s.plugin == nil {
return nil, fmt.Errorf("plugin is not enabled") return nil, fmt.Errorf("plugin is not enabled")
} }

77
weed/admin/plugin/plugin.go

@ -34,6 +34,13 @@ type Options struct {
ClusterContextProvider func(context.Context) (*plugin_pb.ClusterContext, error) 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 { type Plugin struct {
plugin_pb.UnimplementedPluginControlServiceServer plugin_pb.UnimplementedPluginControlServiceServer
@ -623,7 +630,7 @@ func (r *Plugin) ListWorkers() []*WorkerSession {
return r.registry.List() return r.registry.List()
} }
func (r *Plugin) ListKnownJobTypes() ([]string, error) {
func (r *Plugin) ListKnownJobTypes() ([]JobTypeInfo, error) {
registryJobTypes := r.registry.JobTypes() registryJobTypes := r.registry.JobTypes()
storedJobTypes, err := r.store.ListJobTypes() storedJobTypes, err := r.store.ListJobTypes()
if err != nil { if err != nil {
@ -638,12 +645,72 @@ func (r *Plugin) ListKnownJobTypes() ([]string, error) {
jobTypeSet[jobType] = struct{}{} jobTypeSet[jobType] = struct{}{}
} }
out := make([]string, 0, len(jobTypeSet))
jobTypeList := make([]string, 0, len(jobTypeSet))
for jobType := range 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. // FilterProposalsWithActiveJobs drops proposals that are already assigned/running.

4
weed/admin/plugin/plugin_scheduler.go

@ -175,7 +175,8 @@ func (r *Plugin) ListSchedulerStates() ([]SchedulerJobTypeState, error) {
r.schedulerMu.Unlock() r.schedulerMu.Unlock()
states := make([]SchedulerJobTypeState, 0, len(jobTypes)) states := make([]SchedulerJobTypeState, 0, len(jobTypes))
for _, jobType := range jobTypes {
for _, jobTypeInfo := range jobTypes {
jobType := jobTypeInfo.JobType
state := SchedulerJobTypeState{ state := SchedulerJobTypeState{
JobType: jobType, JobType: jobType,
DetectionInFlight: detectionInFlight[jobType], DetectionInFlight: detectionInFlight[jobType],
@ -187,6 +188,7 @@ func (r *Plugin) ListSchedulerStates() ([]SchedulerJobTypeState, error) {
} }
policy, enabled, loadErr := r.loadSchedulerPolicy(jobType) policy, enabled, loadErr := r.loadSchedulerPolicy(jobType)
if loadErr != nil { if loadErr != nil {
state.PolicyError = loadErr.Error() state.PolicyError = loadErr.Error()
} else { } else {

41
weed/admin/view/app/plugin.templ

@ -683,6 +683,25 @@ templ Plugin(page string) {
return 'configuration'; 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) { function topTabKeyForJobType(jobType) {
var normalized = String(jobType || '').trim(); var normalized = String(jobType || '').trim();
if (!normalized) { if (!normalized) {
@ -706,7 +725,8 @@ templ Plugin(page string) {
} }
var jobTypes = Array.isArray(state.jobTypes) ? state.jobTypes : []; var jobTypes = Array.isArray(state.jobTypes) ? state.jobTypes : [];
for (var i = 0; i < jobTypes.length; i++) { 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; return true;
} }
} }
@ -728,7 +748,8 @@ templ Plugin(page string) {
state.activeTopTab = 'overview'; state.activeTopTab = 'overview';
return; return;
} }
state.selectedJobType = String(state.jobTypes[0] || '').trim();
var firstItem = parseJobTypeItem(state.jobTypes[0]);
state.selectedJobType = firstItem.jobType;
state.activeTopTab = topTabKeyForJobType(state.selectedJobType); state.activeTopTab = topTabKeyForJobType(state.selectedJobType);
} }
@ -749,7 +770,8 @@ templ Plugin(page string) {
if (state.activeTopTab === 'overview') { if (state.activeTopTab === 'overview') {
if (!state.selectedJobType && Array.isArray(state.jobTypes) && state.jobTypes.length > 0) { 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; return;
} }
@ -758,7 +780,8 @@ templ Plugin(page string) {
topTabJobType = state.selectedJobType; topTabJobType = state.selectedJobType;
} }
if (!topTabJobType && Array.isArray(state.jobTypes) && state.jobTypes.length > 0) { 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) { if (!topTabJobType) {
state.activeTopTab = 'overview'; state.activeTopTab = 'overview';
@ -784,14 +807,17 @@ templ Plugin(page string) {
var jobTypes = Array.isArray(state.jobTypes) ? state.jobTypes : []; var jobTypes = Array.isArray(state.jobTypes) ? state.jobTypes : [];
for (var i = 0; i < jobTypes.length; i++) { 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) { if (!jobType) {
continue; continue;
} }
html += '' + html += '' +
'<li class="nav-item">' + '<li class="nav-item">' +
'<button type="button" class="nav-link" data-plugin-top-tab="' + escapeHtml(topTabKeyForJobType(jobType)) + '">' + '<button type="button" class="nav-link" data-plugin-top-tab="' + escapeHtml(topTabKeyForJobType(jobType)) + '">' +
escapeHtml(jobType) +
escapeHtml(displayName) +
'</button>' + '</button>' +
'</li>'; '</li>';
} }
@ -1267,7 +1293,8 @@ templ Plugin(page string) {
var jobTypes = []; var jobTypes = [];
for (var k = 0; k < known.length; k++) { 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]) { if (!knownType || seen[knownType]) {
continue; continue;
} }

2
weed/admin/view/app/plugin_templ.go
File diff suppressed because it is too large
View File

1
weed/pb/plugin.proto

@ -96,6 +96,7 @@ message JobTypeCapability {
int32 max_execution_concurrency = 5; int32 max_execution_concurrency = 5;
string display_name = 6; string display_name = 6;
string description = 7; string description = 7;
int32 weight = 8;
} }
message RequestConfigSchema { message RequestConfigSchema {

13
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"` 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"` 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"` 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -1292,6 +1293,13 @@ func (x *JobTypeCapability) GetDescription() string {
return "" return ""
} }
func (x *JobTypeCapability) GetWeight() int32 {
if x != nil {
return x.Weight
}
return 0
}
type RequestConfigSchema struct { type RequestConfigSchema struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
JobType string `protobuf:"bytes,1,opt,name=job_type,json=jobType,proto3" json:"job_type,omitempty"` 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" + "\bjob_type\x18\x03 \x01(\tR\ajobType\x12&\n" +
"\x05state\x18\x04 \x01(\x0e2\x10.plugin.JobStateR\x05state\x12)\n" + "\x05state\x18\x04 \x01(\x0e2\x10.plugin.JobStateR\x05state\x12)\n" +
"\x10progress_percent\x18\x05 \x01(\x01R\x0fprogressPercent\x12\x14\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" + "\x11JobTypeCapability\x12\x19\n" +
"\bjob_type\x18\x01 \x01(\tR\ajobType\x12\x1d\n" + "\bjob_type\x18\x01 \x01(\tR\ajobType\x12\x1d\n" +
"\n" + "\n" +
@ -3958,7 +3966,8 @@ const file_plugin_proto_rawDesc = "" +
"\x19max_detection_concurrency\x18\x04 \x01(\x05R\x17maxDetectionConcurrency\x12:\n" + "\x19max_detection_concurrency\x18\x04 \x01(\x05R\x17maxDetectionConcurrency\x12:\n" +
"\x19max_execution_concurrency\x18\x05 \x01(\x05R\x17maxExecutionConcurrency\x12!\n" + "\x19max_execution_concurrency\x18\x05 \x01(\x05R\x17maxExecutionConcurrency\x12!\n" +
"\fdisplay_name\x18\x06 \x01(\tR\vdisplayName\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" + "\x13RequestConfigSchema\x12\x19\n" +
"\bjob_type\x18\x01 \x01(\tR\ajobType\x12#\n" + "\bjob_type\x18\x01 \x01(\tR\ajobType\x12#\n" +
"\rforce_refresh\x18\x02 \x01(\bR\fforceRefresh\"\xda\x01\n" + "\rforce_refresh\x18\x02 \x01(\bR\fforceRefresh\"\xda\x01\n" +

Loading…
Cancel
Save