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:
Chris Lu
2026-02-26 19:20:48 -08:00
committed by GitHub
parent 8eba7ba5b2
commit c73e65ad5e
7 changed files with 128 additions and 21 deletions

View File

@@ -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;
}