From af460f1c41321704cb02856909faf778b88cf0b6 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Sat, 1 Nov 2025 17:38:08 +0100 Subject: [PATCH] [dashboard-controller] Move bages generation logic to internal dashboard component Signed-off-by: Andrei Kvapil --- .../controller/dashboard/customcolumns.go | 24 +-- internal/controller/dashboard/factory.go | 3 +- internal/controller/dashboard/helpers.go | 100 ------------ .../controller/dashboard/static_helpers.go | 150 ++++++++++-------- .../controller/dashboard/static_refactored.go | 57 ++++--- internal/controller/dashboard/ui_helpers.go | 6 +- .../controller/dashboard/unified_helpers.go | 148 +++++------------ 7 files changed, 155 insertions(+), 333 deletions(-) diff --git a/internal/controller/dashboard/customcolumns.go b/internal/controller/dashboard/customcolumns.go index d737fd14..6c23d68b 100644 --- a/internal/controller/dashboard/customcolumns.go +++ b/internal/controller/dashboard/customcolumns.go @@ -30,10 +30,6 @@ func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1al name := fmt.Sprintf("stock-namespace-%s.%s.%s", g, v, plural) id := fmt.Sprintf("stock-namespace-/%s/%s/%s", g, v, plural) - // Badge content & color derived from kind - badgeText := initialsFromKind(kind) // e.g., "VirtualMachine" -> "VM", "Bucket" -> "B" - badgeColor := hexColorForKind(kind) // deterministic, dark enough for white text - obj := &dashv1alpha1.CustomColumnsOverride{} obj.SetName(name) @@ -62,25 +58,11 @@ func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1al }, "children": []any{ map[string]any{ - "type": "antdText", + "type": "ResourceBadge", "data": map[string]any{ "id": "header-badge", - "text": badgeText, - "title": strings.ToLower(kind), // optional tooltip - "style": map[string]any{ - "backgroundColor": badgeColor, - "borderRadius": "20px", - "color": "#fff", - "display": "inline-block", - "fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif", - "fontSize": "15px", - "fontWeight": 400, - "lineHeight": "24px", - "minWidth": 24, - "padding": "0 9px", - "textAlign": "center", - "whiteSpace": "nowrap", - }, + "value": kind, + // abbreviation auto-generated by ResourceBadge from value }, }, map[string]any{ diff --git a/internal/controller/dashboard/factory.go b/internal/controller/dashboard/factory.go index f5da1d6b..6f3e8f4c 100644 --- a/internal/controller/dashboard/factory.go +++ b/internal/controller/dashboard/factory.go @@ -53,7 +53,6 @@ func (m *Manager) ensureFactory(ctx context.Context, crd *cozyv1alpha1.Cozystack Kind: kind, Plural: plural, Title: strings.ToLower(plural), - Size: BadgeSizeLarge, } spec := createUnifiedFactory(config, tabs, []any{resourceFetch}) @@ -115,7 +114,7 @@ func detailsTab(kind, endpoint, schemaJSON string, keysOrder [][]string) map[str "gap": float64(6), }, "children": []any{ - createUnifiedBadgeFromKind("ns-badge", "Namespace", "namespace", BadgeSizeMedium), + createUnifiedBadgeFromKind("ns-badge", "Namespace"), antdLink("namespace-link", "{reqsJsonPath[0]['.metadata.namespace']['-']}", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace", diff --git a/internal/controller/dashboard/helpers.go b/internal/controller/dashboard/helpers.go index e5361c53..a0023a05 100644 --- a/internal/controller/dashboard/helpers.go +++ b/internal/controller/dashboard/helpers.go @@ -1,12 +1,9 @@ package dashboard import ( - "crypto/sha1" - "encoding/hex" "encoding/json" "fmt" "reflect" - "regexp" "sort" "strings" @@ -57,97 +54,6 @@ func pickPlural(kind string, crd *cozyv1alpha1.CozystackResourceDefinition) stri return k + "s" } -// initialsFromKind splits CamelCase and returns the first letters in upper case. -// "VirtualMachine" -> "VM"; "Bucket" -> "B". -func initialsFromKind(kind string) string { - parts := splitCamel(kind) - if len(parts) == 0 { - return strings.ToUpper(kind) - } - var b strings.Builder - for _, p := range parts { - if p == "" { - continue - } - b.WriteString(strings.ToUpper(string(p[0]))) - // Limit to 3 chars to keep the badge compact (VM, PVC, etc.) - if b.Len() >= 3 { - break - } - } - return b.String() -} - -// hexColorForKind returns a dark, saturated color (hex) derived from a stable hash of the kind. -// We map the hash to an HSL hue; fix S/L for consistent readability with white text. -func hexColorForKind(kind string) string { - // Stable short hash (sha1 → bytes → hue) - sum := sha1.Sum([]byte(kind)) - // Use first two bytes for hue [0..359] - hue := int(sum[0])<<8 | int(sum[1]) - hue = hue % 360 - - // Fixed S/L chosen to contrast with white text: - // S = 80%, L = 35% (dark enough so #fff is readable) - r, g, b := hslToRGB(float64(hue), 0.80, 0.35) - - return fmt.Sprintf("#%02x%02x%02x", r, g, b) -} - -// hslToRGB converts HSL (0..360, 0..1, 0..1) to sRGB (0..255). -func hslToRGB(h float64, s float64, l float64) (uint8, uint8, uint8) { - c := (1 - absFloat(2*l-1)) * s - hp := h / 60.0 - x := c * (1 - absFloat(modFloat(hp, 2)-1)) - var r1, g1, b1 float64 - switch { - case 0 <= hp && hp < 1: - r1, g1, b1 = c, x, 0 - case 1 <= hp && hp < 2: - r1, g1, b1 = x, c, 0 - case 2 <= hp && hp < 3: - r1, g1, b1 = 0, c, x - case 3 <= hp && hp < 4: - r1, g1, b1 = 0, x, c - case 4 <= hp && hp < 5: - r1, g1, b1 = x, 0, c - default: - r1, g1, b1 = c, 0, x - } - m := l - c/2 - r := uint8(clamp01(r1+m) * 255.0) - g := uint8(clamp01(g1+m) * 255.0) - b := uint8(clamp01(b1+m) * 255.0) - return r, g, b -} - -func absFloat(v float64) float64 { - if v < 0 { - return -v - } - return v -} - -func modFloat(a, b float64) float64 { - return a - b*float64(int(a/b)) -} - -func clamp01(v float64) float64 { - if v < 0 { - return 0 - } - if v > 1 { - return 1 - } - return v -} - -// optional: tiny helper to expose the compact color hash (useful for debugging) -func shortHashHex(s string) string { - sum := sha1.Sum([]byte(s)) - return hex.EncodeToString(sum[:4]) -} - // ----------------------- Helpers (OpenAPI → values) ----------------------- // defaultOrZero returns the schema default if present; otherwise a reasonable zero value. @@ -295,12 +201,6 @@ func normalizeJSON(v any) any { } } -var camelSplitter = regexp.MustCompile(`(?m)([A-Z]+[a-z0-9]*|[a-z0-9]+)`) - -func splitCamel(s string) []string { - return camelSplitter.FindAllString(s, -1) -} - // --- helpers for schema inspection --- func isScalarType(n map[string]any) bool { diff --git a/internal/controller/dashboard/static_helpers.go b/internal/controller/dashboard/static_helpers.go index 469e9f8f..5affc1c1 100644 --- a/internal/controller/dashboard/static_helpers.go +++ b/internal/controller/dashboard/static_helpers.go @@ -531,7 +531,6 @@ func createBreadcrumbItem(key, label string, link ...string) map[string]any { // createCustomColumn creates a custom column with factory type and badge func createCustomColumn(name, kind, plural, href string) map[string]any { - badge := createUnifiedBadgeFromKind("header-badge", kind, plural, BadgeSizeMedium) link := antdLink("name-link", "{reqsJsonPath[0]['.metadata.name']['-']}", href) return map[string]any{ @@ -541,8 +540,18 @@ func createCustomColumn(name, kind, plural, href string) map[string]any { "disableEventBubbling": true, "items": []any{ map[string]any{ - "children": []any{badge, link}, - "type": "antdFlex", + "children": []any{ + map[string]any{ + "type": "ResourceBadge", + "data": map[string]any{ + "id": "header-badge", + "value": kind, + // abbreviation auto-generated by ResourceBadge from value + }, + }, + link, + }, + "type": "antdFlex", "data": map[string]any{ "align": "center", "gap": float64(6), @@ -554,16 +563,16 @@ func createCustomColumn(name, kind, plural, href string) map[string]any { } // createCustomColumnWithBadge creates a custom column with a specific badge -func createCustomColumnWithBadge(name, badgeText, badgeColor, title, href string) map[string]any { - config := BadgeConfig{ - Text: badgeText, - Color: badgeColor, - Title: title, - Size: BadgeSizeMedium, - } - badge := createUnifiedBadge("header-badge", config) +// badgeValue should be the kind in PascalCase (e.g., "Service", "Pod") +// abbreviation is auto-generated by ResourceBadge from badgeValue +func createCustomColumnWithBadge(name, badgeValue, href string) map[string]any { link := antdLink("name-link", "{reqsJsonPath[0]['.metadata.name']['-']}", href) + badgeData := map[string]any{ + "id": "header-badge", + "value": badgeValue, + } + return map[string]any{ "name": name, "type": "factory", @@ -571,8 +580,14 @@ func createCustomColumnWithBadge(name, badgeText, badgeColor, title, href string "disableEventBubbling": true, "items": []any{ map[string]any{ - "children": []any{badge, link}, - "type": "antdFlex", + "children": []any{ + map[string]any{ + "type": "ResourceBadge", + "data": badgeData, + }, + link, + }, + "type": "antdFlex", "data": map[string]any{ "align": "center", "gap": float64(6), @@ -583,17 +598,22 @@ func createCustomColumnWithBadge(name, badgeText, badgeColor, title, href string } } -// createCustomColumnWithSpecificColor creates a custom column with a specific color -func createCustomColumnWithSpecificColor(name, kind, title, color, href string) map[string]any { - config := BadgeConfig{ - Text: initialsFromKind(kind), - Color: color, - Title: title, - Size: BadgeSizeMedium, - } - badge := createUnifiedBadge("header-badge", config) +// createCustomColumnWithSpecificColor creates a custom column with a specific kind and optional color +// badgeValue should be the kind in PascalCase (e.g., "Service", "Pod") +func createCustomColumnWithSpecificColor(name, kind, color, href string) map[string]any { link := antdLink("name-link", "{reqsJsonPath[0]['.metadata.name']['-']}", href) + badgeData := map[string]any{ + "id": "header-badge", + "value": kind, + } + // Add custom color if specified + if color != "" { + badgeData["style"] = map[string]any{ + "backgroundColor": color, + } + } + return map[string]any{ "name": name, "type": "factory", @@ -602,8 +622,14 @@ func createCustomColumnWithSpecificColor(name, kind, title, color, href string) "disableEventBubbling": true, "items": []any{ map[string]any{ - "children": []any{badge, link}, - "type": "antdFlex", + "children": []any{ + map[string]any{ + "type": "ResourceBadge", + "data": badgeData, + }, + link, + }, + "type": "antdFlex", "data": map[string]any{ "align": "center", "gap": float64(6), @@ -668,7 +694,7 @@ func createTimestampColumn(name, jsonPath string) map[string]any { // createFactoryHeader creates a header for factory resources func createFactoryHeader(kind, plural string) map[string]any { lowerKind := strings.ToLower(kind) - badge := createUnifiedBadgeFromKind("badge-"+lowerKind, kind, plural, BadgeSizeLarge) + badge := createUnifiedBadgeFromKind("badge-"+lowerKind, kind) nameText := parsedText(lowerKind+"-name", "{reqsJsonPath[0]['.metadata.name']['-']}", map[string]any{ "fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif", "fontSize": float64(20), @@ -718,13 +744,26 @@ func createFactorySpec(key string, sidebarTags []any, urlsToFetch []any, header } // createCustomColumnWithJsonPath creates a column with a custom badge and link using jsonPath -func createCustomColumnWithJsonPath(name, jsonPath, badgeText, badgeTitle, badgeColor, linkHref string) map[string]any { +// badgeValue should be the kind in PascalCase (e.g., "Service", "VirtualMachine") +// abbreviation is auto-generated by ResourceBadge from badgeValue +func createCustomColumnWithJsonPath(name, jsonPath, badgeValue, badgeColor, linkHref string) map[string]any { // Determine link ID based on jsonPath linkId := "name-link" if jsonPath == ".metadata.namespace" { linkId = "namespace-link" } + badgeData := map[string]any{ + "id": "header-badge", + "value": badgeValue, + } + // Add custom color if specified + if badgeColor != "" { + badgeData["style"] = map[string]any{ + "backgroundColor": badgeColor, + } + } + return map[string]any{ "name": name, "type": "factory", @@ -741,26 +780,8 @@ func createCustomColumnWithJsonPath(name, jsonPath, badgeText, badgeTitle, badge }, "children": []any{ map[string]any{ - "type": "antdText", - "data": map[string]any{ - "id": "header-badge", - "text": badgeText, - "title": badgeTitle, - "style": map[string]any{ - "backgroundColor": badgeColor, - "borderRadius": "20px", - "color": "#fff", - "display": "inline-block", - "fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif", - "fontSize": "15px", - "fontWeight": 400, - "lineHeight": "24px", - "minWidth": 24, - "padding": "0 9px", - "textAlign": "center", - "whiteSpace": "nowrap", - }, - }, + "type": "ResourceBadge", + "data": badgeData, }, map[string]any{ "type": "antdLink", @@ -778,7 +799,20 @@ func createCustomColumnWithJsonPath(name, jsonPath, badgeText, badgeTitle, badge } // createCustomColumnWithoutJsonPath creates a column with a custom badge and link without jsonPath -func createCustomColumnWithoutJsonPath(name, badgeText, badgeTitle, badgeColor, linkHref string) map[string]any { +// badgeValue should be the kind in PascalCase (e.g., "Node", "Pod") +// abbreviation is auto-generated by ResourceBadge from badgeValue +func createCustomColumnWithoutJsonPath(name, badgeValue, badgeColor, linkHref string) map[string]any { + badgeData := map[string]any{ + "id": "header-badge", + "value": badgeValue, + } + // Add custom color if specified + if badgeColor != "" { + badgeData["style"] = map[string]any{ + "backgroundColor": badgeColor, + } + } + return map[string]any{ "name": name, "type": "factory", @@ -794,26 +828,8 @@ func createCustomColumnWithoutJsonPath(name, badgeText, badgeTitle, badgeColor, }, "children": []any{ map[string]any{ - "type": "antdText", - "data": map[string]any{ - "id": "header-badge", - "text": badgeText, - "title": badgeTitle, - "style": map[string]any{ - "backgroundColor": badgeColor, - "borderRadius": "20px", - "color": "#fff", - "display": "inline-block", - "fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif", - "fontSize": "15px", - "fontWeight": 400, - "lineHeight": "24px", - "minWidth": 24, - "padding": "0 9px", - "textAlign": "center", - "whiteSpace": "nowrap", - }, - }, + "type": "ResourceBadge", + "data": badgeData, }, map[string]any{ "type": "antdLink", diff --git a/internal/controller/dashboard/static_refactored.go b/internal/controller/dashboard/static_refactored.go index af9c5449..9ad923e7 100644 --- a/internal/controller/dashboard/static_refactored.go +++ b/internal/controller/dashboard/static_refactored.go @@ -132,7 +132,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid return []*dashboardv1alpha1.CustomColumnsOverride{ // Factory details v1 services createCustomColumnsOverride("factory-details-v1.services", []any{ - createCustomColumnWithSpecificColor("Name", "Service", "service", getColorForType("service"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-service-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithSpecificColor("Name", "Service", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-service-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createStringColumn("ClusterIP", ".spec.clusterIP"), createStringColumn("LoadbalancerIP", ".spec.loadBalancerIP"), createTimestampColumn("Created", ".metadata.creationTimestamp"), @@ -140,7 +140,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Stock namespace v1 services createCustomColumnsOverride("stock-namespace-/v1/services", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "S", "service", getColorForType("service"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-service-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Service", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-service-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createStringColumn("ClusterIP", ".spec.clusterIP"), createStringColumn("LoadbalancerIP", ".status.loadBalancer.ingress[0].ip"), createTimestampColumn("Created", ".metadata.creationTimestamp"), @@ -148,7 +148,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Stock namespace core cozystack io v1alpha1 tenantmodules createCustomColumnsOverride("stock-namespace-/core.cozystack.io/v1alpha1/tenantmodules", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "M", "module", getColorForType("module"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/{reqsJsonPath[0]['.metadata.name']['-']}-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Module", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/{reqsJsonPath[0]['.metadata.name']['-']}-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createReadyColumn(), createTimestampColumn("Created", ".metadata.creationTimestamp"), createStringColumn("Version", ".status.version"), @@ -164,7 +164,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Factory details v1alpha1 cozystack io workloadmonitors createCustomColumnsOverride("factory-details-v1alpha1.cozystack.io.workloadmonitors", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "W", "workloadmonitor", getColorForType("workloadmonitor"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/workloadmonitor-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "WorkloadMonitor", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/workloadmonitor-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createStringColumn("TYPE", ".spec.type"), createStringColumn("VERSION", ".spec.version"), createStringColumn("REPLICAS", ".spec.replicas"), @@ -175,7 +175,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Factory details v1alpha1 core cozystack io tenantsecretstables createCustomColumnsOverride("factory-details-v1alpha1.core.cozystack.io.tenantsecretstables", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "S", "secret", getColorForType("secret"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-secret-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Secret", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-secret-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createStringColumn("Key", ".data.key"), createSecretBase64Column("Value", ".data.value"), createTimestampColumn("Created", ".metadata.creationTimestamp"), @@ -184,7 +184,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Factory ingress details rules createCustomColumnsOverride("factory-kube-ingress-details-rules", []any{ createStringColumn("Host", ".host"), - createCustomColumnWithJsonPath("Service", ".http.paths[0].backend.service.name", "S", "service", getColorForType("service"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-service-details/{reqsJsonPath[0]['.http.paths[0].backend.service.name']['-']}"), + createCustomColumnWithJsonPath("Service", ".http.paths[0].backend.service.name", "Service", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-service-details/{reqsJsonPath[0]['.http.paths[0].backend.service.name']['-']}"), createStringColumn("Port", ".http.paths[0].backend.service.port.number"), createStringColumn("Path", ".http.paths[0].path"), }), @@ -250,7 +250,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Factory details networking k8s io v1 ingresses createCustomColumnsOverride("factory-details-networking.k8s.io.v1.ingresses", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "I", "ingress", getColorForType("ingress"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-ingress-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Ingress", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-ingress-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createStringColumn("Hosts", ".spec.rules[*].host"), createStringColumn("Address", ".status.loadBalancer.ingress[0].ip"), createStringColumn("Port", ".spec.defaultBackend.service.port.number"), @@ -259,7 +259,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Stock namespace networking k8s io v1 ingresses createCustomColumnsOverride("stock-namespace-/networking.k8s.io/v1/ingresses", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "I", "ingress", getColorForType("ingress"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-ingress-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Ingress", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-ingress-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createStringColumn("Hosts", ".spec.rules[*].host"), createStringColumn("Address", ".status.loadBalancer.ingress[0].ip"), createStringColumn("Port", ".spec.defaultBackend.service.port.number"), @@ -268,34 +268,34 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Stock cluster v1 configmaps createCustomColumnsOverride("stock-cluster-/v1/configmaps", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "CM", "configmap", getColorForType("configmap"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/configmap-details/{reqsJsonPath[0]['.metadata.name']['-']}"), - createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "NS", "namespace", getColorForType("namespace"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "ConfigMap", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/configmap-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "Namespace", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace"), createTimestampColumn("Created", ".metadata.creationTimestamp"), }), // Stock namespace v1 configmaps createCustomColumnsOverride("stock-namespace-/v1/configmaps", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "CM", "configmap", getColorForType("configmap"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/configmap-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "ConfigMap", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/configmap-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createTimestampColumn("Created", ".metadata.creationTimestamp"), }), // Cluster v1 configmaps createCustomColumnsOverride("cluster-/v1/configmaps", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "CM", "configmap", getColorForType("configmap"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/configmap-details/{reqsJsonPath[0]['.metadata.name']['-']}"), - createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "NS", "namespace", getColorForType("namespace"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "ConfigMap", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/configmap-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "Namespace", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace"), createTimestampColumn("Created", ".metadata.creationTimestamp"), }), // Stock cluster v1 nodes createCustomColumnsOverride("stock-cluster-/v1/nodes", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "N", "node", getColorForType("node"), "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Node", "", "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createSimpleStatusColumn("Status", "node-status"), }), // Factory node details v1 pods createCustomColumnsOverride("factory-node-details-v1.pods", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "P", "pod", getColorForType("pod"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), - createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "NS", "namespace", getColorForType("namespace"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Pod", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "Namespace", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace"), createStringColumn("Restart Policy", ".spec.restartPolicy"), createStringColumn("Pod IP", ".status.podIP"), createStringColumn("QOS", ".status.qosClass"), @@ -304,8 +304,8 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Factory v1 pods createCustomColumnsOverride("factory-v1.pods", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "P", "pod", getColorForType("pod"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), - createCustomColumnWithoutJsonPath("Node", "N", "node", getColorForType("node"), "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.spec.nodeName']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Pod", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithoutJsonPath("Node", "Node", "", "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.spec.nodeName']['-']}"), createStringColumn("Restart Policy", ".spec.restartPolicy"), createStringColumn("Pod IP", ".status.podIP"), createStringColumn("QOS", ".status.qosClass"), @@ -314,9 +314,9 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Stock cluster v1 pods createCustomColumnsOverride("stock-cluster-/v1/pods", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "P", "pod", "#009596", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), - createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "NS", "namespace", "#a25792ff", "/openapi-ui/{2}/factory/tenantnamespace/{reqsJsonPath[0]['.metadata.namespace']['-']}"), - createCustomColumnWithJsonPath("Node", ".spec.nodeName", "N", "node", "#8476d1", "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.spec.nodeName']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Pod", "#009596", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "Namespace", "#a25792ff", "/openapi-ui/{2}/factory/tenantnamespace/{reqsJsonPath[0]['.metadata.namespace']['-']}"), + createCustomColumnWithJsonPath("Node", ".spec.nodeName", "Node", "#8476d1", "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.spec.nodeName']['-']}"), createStringColumn("Restart Policy", ".spec.restartPolicy"), createStringColumn("Pod IP", ".status.podIP"), createStringColumn("QOS", ".status.qosClass"), @@ -325,8 +325,8 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Stock namespace v1 pods createCustomColumnsOverride("stock-namespace-/v1/pods", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "P", "pod", "#009596", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), - createCustomColumnWithoutJsonPath("Node", "N", "node", "#8476d1", "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.spec.nodeName']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Pod", "#009596", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/pod-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithoutJsonPath("Node", "Node", "#8476d1", "/openapi-ui/{2}/factory/node-details/{reqsJsonPath[0]['.spec.nodeName']['-']}"), createStringColumn("Restart Policy", ".spec.restartPolicy"), createStringColumn("Pod IP", ".status.podIP"), createStringColumn("QOS", ".status.qosClass"), @@ -335,15 +335,15 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Stock cluster v1 secrets createCustomColumnsOverride("stock-cluster-/v1/secrets", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "S", "secret", "#c46100", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-secret-details/{reqsJsonPath[0]['.metadata.name']['-']}"), - createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "NS", "namespace", "#a25792ff", "/openapi-ui/{2}/factory/tenantnamespace/{reqsJsonPath[0]['.metadata.namespace']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Secret", "#c46100", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-secret-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Namespace", ".metadata.namespace", "Namespace", "#a25792ff", "/openapi-ui/{2}/factory/tenantnamespace/{reqsJsonPath[0]['.metadata.namespace']['-']}"), createStringColumn("Type", ".type"), createTimestampColumn("Created", ".metadata.creationTimestamp"), }), // Stock namespace v1 secrets createCustomColumnsOverride("stock-namespace-/v1/secrets", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "S", "secret", "#c46100", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-secret-details/{reqsJsonPath[0]['.metadata.name']['-']}"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "Secret", "#c46100", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-secret-details/{reqsJsonPath[0]['.metadata.name']['-']}"), createStringColumn("Type", ".type"), createTimestampColumn("Created", ".metadata.creationTimestamp"), }), @@ -360,7 +360,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid // Stock cluster core cozystack io v1alpha1 tenantnamespaces createCustomColumnsOverride("stock-cluster-/core.cozystack.io/v1alpha1/tenantnamespaces", []any{ - createCustomColumnWithJsonPath("Name", ".metadata.name", "TN", "tenantnamespace", getColorForType("tenantnamespace"), "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.name']['-']}/factory/marketplace"), + createCustomColumnWithJsonPath("Name", ".metadata.name", "TenantNamespace", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.name']['-']}/factory/marketplace"), createTimestampColumn("Created", ".metadata.creationTimestamp"), }), } @@ -496,7 +496,6 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { Kind: "Namespace", Plural: "namespaces", Title: "namespace", - Size: BadgeSizeLarge, } namespaceSpec := createUnifiedFactory(namespaceConfig, nil, []any{"/api/clusters/{2}/k8s/api/v1/namespaces/{5}"}) @@ -1202,7 +1201,7 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory { "gap": 6, }, "children": []any{ - createUnifiedBadgeFromKind("ns-badge", "Namespace", "namespace", BadgeSizeMedium), + createUnifiedBadgeFromKind("ns-badge", "Namespace"), antdLink("namespace-link", "{reqsJsonPath[0]['.metadata.namespace']['-']}", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace", diff --git a/internal/controller/dashboard/ui_helpers.go b/internal/controller/dashboard/ui_helpers.go index 4066fd36..fb29a608 100644 --- a/internal/controller/dashboard/ui_helpers.go +++ b/internal/controller/dashboard/ui_helpers.go @@ -1,7 +1,5 @@ package dashboard -import "strings" - // ---------------- UI helpers (use float64 for numeric fields) ---------------- func contentCard(id string, style map[string]any, children []any) map[string]any { @@ -200,10 +198,10 @@ func createBadge(id, text, color, title string) map[string]any { // createBadgeFromKind creates a badge using the existing badge generation functions func createBadgeFromKind(id, kind, title string) map[string]any { - return createUnifiedBadgeFromKind(id, kind, title, BadgeSizeMedium) + return createUnifiedBadgeFromKind(id, kind) } // createHeaderBadge creates a badge specifically for headers with consistent styling func createHeaderBadge(id, kind, plural string) map[string]any { - return createUnifiedBadgeFromKind(id, kind, strings.ToLower(plural), BadgeSizeLarge) + return createUnifiedBadgeFromKind(id, kind) } diff --git a/internal/controller/dashboard/unified_helpers.go b/internal/controller/dashboard/unified_helpers.go index 7bdea2b5..b25d5b65 100644 --- a/internal/controller/dashboard/unified_helpers.go +++ b/internal/controller/dashboard/unified_helpers.go @@ -81,86 +81,47 @@ func isAlphanumeric(c byte) bool { // BadgeConfig holds configuration for badge generation type BadgeConfig struct { - Text string - Color string - Title string - Size BadgeSize + Kind string // Resource kind in PascalCase (e.g., "VirtualMachine") - used for value and auto-generation + Text string // Optional abbreviation override (if empty, ResourceBadge auto-generates from Kind) + Color string // Optional custom backgroundColor override } -// BadgeSize represents the size of the badge -type BadgeSize int - -const ( - BadgeSizeSmall BadgeSize = iota - BadgeSizeMedium - BadgeSizeLarge -) - -// generateBadgeConfig creates a BadgeConfig from kind and optional custom values -func generateBadgeConfig(kind string, customText, customColor, customTitle string) BadgeConfig { - config := BadgeConfig{ - Text: initialsFromKind(kind), - Color: hexColorForKind(kind), - Title: strings.ToLower(kind), - Size: BadgeSizeMedium, - } - - // Override with custom values if provided - if customText != "" { - config.Text = customText - } - if customColor != "" { - config.Color = customColor - } - if customTitle != "" { - config.Title = customTitle - } - - return config -} - -// createUnifiedBadge creates a badge using the unified BadgeConfig +// createUnifiedBadge creates a badge using the unified BadgeConfig with ResourceBadge component func createUnifiedBadge(id string, config BadgeConfig) map[string]any { - fontSize := "15px" - if config.Size == BadgeSizeLarge { - fontSize = "20px" - } else if config.Size == BadgeSizeSmall { - fontSize = "12px" + data := map[string]any{ + "id": id, + "value": config.Kind, + } + + // Add abbreviation override if specified (otherwise ResourceBadge auto-generates from Kind) + if config.Text != "" { + data["abbreviation"] = config.Text + } + + // Add custom color if specified + if config.Color != "" { + data["style"] = map[string]any{ + "backgroundColor": config.Color, + } } return map[string]any{ - "type": "antdText", - "data": map[string]any{ - "id": id, - "text": config.Text, - "title": config.Title, - "style": map[string]any{ - "backgroundColor": config.Color, - "borderRadius": "20px", - "color": "#fff", - "display": "inline-block", - "fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif", - "fontSize": fontSize, - "fontWeight": float64(400), - "lineHeight": "24px", - "minWidth": float64(24), - "padding": "0 9px", - "textAlign": "center", - "whiteSpace": "nowrap", - }, - }, + "type": "ResourceBadge", + "data": data, } } -// createUnifiedBadgeFromKind creates a badge from kind with automatic color generation -func createUnifiedBadgeFromKind(id, kind, title string, size BadgeSize) map[string]any { - config := BadgeConfig{ - Text: initialsFromKind(kind), - Color: hexColorForKind(kind), - Title: title, - Size: size, +// createUnifiedBadgeFromKind creates a badge from kind with ResourceBadge component +// Abbreviation is auto-generated by ResourceBadge from kind, but can be customized if needed +func createUnifiedBadgeFromKind(id, kind string) map[string]any { + return map[string]any{ + "type": "ResourceBadge", + "data": map[string]any{ + "id": id, + "value": kind, + // abbreviation is optional - ResourceBadge auto-generates from value + }, } - return createUnifiedBadge(id, config) } // ---------------- Resource creation helpers with unified approach ---------------- @@ -183,7 +144,9 @@ func createResourceConfig(components []string, kind, title string) ResourceConfi metadataName := generateMetadataName(specID) // Generate badge config - badgeConfig := generateBadgeConfig(kind, "", "", title) + badgeConfig := BadgeConfig{ + Kind: kind, + } return ResourceConfig{ SpecID: specID, @@ -196,35 +159,6 @@ func createResourceConfig(components []string, kind, title string) ResourceConfi // ---------------- Enhanced color generation ---------------- -// getColorForKind returns a color for a specific kind with improved distribution -func getColorForKind(kind string) string { - // Use existing hexColorForKind function - return hexColorForKind(kind) -} - -// getColorForType returns a color for a specific type (like "namespace", "service", etc.) -func getColorForType(typeName string) string { - // Map common types to specific colors for consistency - colorMap := map[string]string{ - "namespace": "#a25792ff", - "service": "#6ca100", - "pod": "#009596", - "node": "#8476d1", - "secret": "#c46100", - "configmap": "#b48c78ff", - "ingress": "#2e7dff", - "workloadmonitor": "#c46100", - "module": "#8b5cf6", - } - - if color, exists := colorMap[strings.ToLower(typeName)]; exists { - return color - } - - // Fall back to hash-based color generation - return hexColorForKind(typeName) -} - // ---------------- Automatic ID generation for UI elements ---------------- // generateElementID creates an ID for UI elements based on context and type @@ -282,7 +216,6 @@ type UnifiedResourceConfig struct { Title string Color string BadgeText string - Size BadgeSize } // createUnifiedFactory creates a factory using unified approach @@ -292,16 +225,9 @@ func createUnifiedFactory(config UnifiedResourceConfig, tabs []any, urlsToFetch // Create header with unified badge badgeConfig := BadgeConfig{ + Kind: config.Kind, Text: config.BadgeText, Color: config.Color, - Title: config.Title, - Size: config.Size, - } - if badgeConfig.Text == "" { - badgeConfig.Text = initialsFromKind(config.Kind) - } - if badgeConfig.Color == "" { - badgeConfig.Color = getColorForKind(config.Kind) } badge := createUnifiedBadge(generateBadgeID("header", config.Kind), badgeConfig) @@ -348,7 +274,9 @@ func createUnifiedFactory(config UnifiedResourceConfig, tabs []any, urlsToFetch // createUnifiedCustomColumn creates a custom column using unified approach func createUnifiedCustomColumn(name, jsonPath, kind, title, href string) map[string]any { - badgeConfig := generateBadgeConfig(kind, "", "", title) + badgeConfig := BadgeConfig{ + Kind: kind, + } badge := createUnifiedBadge(generateBadgeID("column", kind), badgeConfig) linkID := generateLinkID("column", "name")