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
master
Chris Lu 21 hours 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 (
"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")
}

77
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.

4
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 {

41
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 += '' +
'<li class="nav-item">' +
'<button type="button" class="nav-link" data-plugin-top-tab="' + escapeHtml(topTabKeyForJobType(jobType)) + '">' +
escapeHtml(jobType) +
escapeHtml(displayName) +
'</button>' +
'</li>';
}
@ -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;
}

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;
string display_name = 6;
string description = 7;
int32 weight = 8;
}
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"`
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" +

Loading…
Cancel
Save