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
This commit is contained in:
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(out)
|
sort.Strings(jobTypeList)
|
||||||
return out, nil
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -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 {
|
||||||
|
|||||||
@@ -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" +
|
||||||
|
|||||||
Reference in New Issue
Block a user