Files
Myasnikov Daniil fc5e6e17dc fix(platform,dashboard): wire OpenSearch into PaaS bundle and form overrides
The opensearch chart, operator chart, ApplicationDefinition and
PackageSources have shipped in the repo since the original OpenSearch
series, but the PaaS bundle template never references the
opensearch-operator / opensearch-application PackageSources. On isp-*
variants with bundles.paas.enabled=true, neither the operator nor the
ApplicationDefinition is ever installed, so the dashboard catalog has
no OpenSearch entry and tenants cannot create
opensearches.apps.cozystack.io resources.

This patch:
  - adds opensearch-operator and opensearch-application to the PaaS
    bundle template, alongside every other DB app
  - adds OpenSearch to the StorageClass listInput override case in
    customformsoverride.go (with matching test entry), so the form's
    storageClass field renders as a StorageClass dropdown in the
    release-1.3 (legacy openapi-ui) dashboard, matching every other
    DB app

Note: only the bundle half is needed on main (release-1.4) because the
new cozystack-ui derives the StorageClass widget client-side from the
schema and ignores CustomFormsOverride; this combined patch targets
release-1.3 directly.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-14 10:33:35 +05:00
..

Dashboard Resource Integration Guide

This guide explains how to add a new Kubernetes resource to the Cozystack dashboard. The dashboard provides a unified interface for viewing and managing Kubernetes resources through custom table views, detail pages, and sidebar navigation.

Overview

Adding a new resource to the dashboard requires three main components:

  1. CustomColumnsOverride: Defines how the resource appears in list/table views
  2. Factory: Defines the detail page layout for individual resource instances
  3. Sidebar Entry: Adds navigation to the resource in the sidebar menu

Prerequisites

  • The resource must have a Kubernetes CustomResourceDefinition (CRD) or be a built-in Kubernetes resource
  • Know the resource's API group, version, kind, and plural name
  • Understand the resource's spec structure to display relevant fields

Step-by-Step Guide

Step 1: Add CustomColumnsOverride

The CustomColumnsOverride defines the columns shown in the resource list table and how clicking on a row navigates to the detail page.

Location: internal/controller/dashboard/static_refactored.go in CreateAllCustomColumnsOverrides()

Example:

// Stock namespace backups cozystack io v1alpha1 plans
createCustomColumnsOverride("stock-namespace-/backups.cozystack.io/v1alpha1/plans", []any{
    createCustomColumnWithJsonPath("Name", ".metadata.name", "Plan", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/plan-details/{reqsJsonPath[0]['.metadata.name']['-']}"),
    createApplicationRefColumn("Application"),
    createTimestampColumn("Created", ".metadata.creationTimestamp"),
}),

Key Components:

  • ID Format: stock-namespace-/{group}/{version}/{plural}
  • Name Column: Use createCustomColumnWithJsonPath() with:
    • Badge value: Resource kind in PascalCase (e.g., "Plan", "Service")
    • Link href: /openapi-ui/{2}/{namespace}/factory/{resource-details}/{name}
  • Additional Columns: Use helper functions like:
    • createStringColumn(): Simple string values
    • createTimestampColumn(): Timestamp fields
    • createReadyColumn(): Ready status from conditions
    • Custom helpers for complex fields

Helper Functions Available:

  • createCustomColumnWithJsonPath(name, jsonPath, badgeValue, badgeColor, linkHref): Column with badge and link
  • createStringColumn(name, jsonPath): Simple string column
  • createTimestampColumn(name, jsonPath): Timestamp with formatting
  • createReadyColumn(): Ready status column
  • createBoolColumn(name, jsonPath): Boolean column
  • createArrayColumn(name, jsonPath): Array column

Step 2: Add Factory (Detail Page)

The Factory defines the detail page layout when viewing an individual resource instance.

Location: internal/controller/dashboard/static_refactored.go in CreateAllFactories()

Using Unified Factory Approach (Recommended):

// Resource details factory using unified approach
resourceConfig := UnifiedResourceConfig{
    Name:         "resource-details",  // Must match the href in CustomColumnsOverride
    ResourceType: "factory",
    Kind:         "ResourceKind",      // PascalCase
    Plural:       "resources",         // lowercase plural
    Title:        "resource",          // lowercase singular
}
resourceTabs := []any{
    map[string]any{
        "key":   "details",
        "label": "Details",
        "children": []any{
            contentCard("details-card", map[string]any{
                "marginBottom": "24px",
            }, []any{
                antdText("details-title", true, "Resource details", map[string]any{
                    "fontSize":     20,
                    "marginBottom": "12px",
                }),
                spacer("details-spacer", 16),
                antdRow("details-grid", []any{48, 12}, []any{
                    antdCol("col-left", 12, []any{
                        antdFlexVertical("col-left-stack", 24, []any{
                            // Metadata fields: Name, Namespace, Created, etc.
                        }),
                    }),
                    antdCol("col-right", 12, []any{
                        antdFlexVertical("col-right-stack", 24, []any{
                            // Spec fields
                        }),
                    }),
                }),
            }),
        },
    },
}
resourceSpec := createUnifiedFactory(resourceConfig, resourceTabs, []any{"/api/clusters/{2}/k8s/apis/{group}/{version}/namespaces/{3}/{plural}/{6}"})

// Add to return list
return []*dashboardv1alpha1.Factory{
    // ... other factories
    createFactory("resource-details", resourceSpec),
}

Key Components:

  • Factory Key: Must match the href path segment (e.g., plan-details matches /factory/plan-details/...)
  • API Endpoint: /api/clusters/{2}/k8s/apis/{group}/{version}/namespaces/{3}/{plural}/{6}
    • {2}: Cluster name
    • {3}: Namespace
    • {6}: Resource name
  • Sidebar Tags: Automatically set to ["{lowercase-kind}-sidebar"] by createUnifiedFactory()
  • Tabs: Define the detail page content (Details, YAML, etc.)

UI Helper Functions:

  • contentCard(id, style, children): Container card
  • antdText(id, strong, text, style): Text element
  • antdFlexVertical(id, gap, children): Vertical flex container
  • antdRow(id, gutter, children): Row layout
  • antdCol(id, span, children): Column layout
  • parsedText(id, text, style): Text with JSON path parsing
  • parsedTextWithFormatter(id, text, formatter): Formatted text (e.g., timestamp)
  • spacer(id, space): Spacing element

Displaying Fields:

antdFlexVertical("field-block", 4, []any{
    antdText("field-label", true, "Field Label", nil),
    parsedText("field-value", "{reqsJsonPath[0]['.spec.fieldName']['-']}", nil),
}),

Step 3: Add Sidebar Entry

The sidebar entry adds navigation to the resource in the sidebar menu.

Location: internal/controller/dashboard/sidebar.go in ensureSidebar()

3a. Add to keysAndTags (around line 110-116):

// Add sidebar for {group} {kind} resource
keysAndTags["{plural}"] = []any{"{lowercase-kind}-sidebar"}

3b. Add Sidebar Section (if creating a new section, around line 169):

// Add hardcoded {SectionName} section
menuItems = append(menuItems, map[string]any{
    "key":   "{section-key}",
    "label": "{SectionName}",
    "children": []any{
        map[string]any{
            "key":   "{plural}",
            "label": "{ResourceLabel}",
            "link":  "/openapi-ui/{cluster}/{namespace}/api-table/{group}/{version}/{plural}",
        },
    },
}),

3c. Add Sidebar ID to targetIDs (around line 220):

"stock-project-factory-{lowercase-kind}-details",

3d. Update Category Ordering (if adding a new section):

  • Update the comment around line 29 to include the new section
  • Update orderCategoryLabels() function if needed
  • Add skip condition in the category loop (around line 149)

Important Notes:

  • The sidebar tag ({lowercase-kind}-sidebar) must match what the Factory uses
  • The link format: /openapi-ui/{cluster}/{namespace}/api-table/{group}/{version}/{plural}
  • All sidebars share the same keysAndTags and menuItems, so changes affect all sidebar instances

Step 4: Verify Integration

  1. Check Factory-Sidebar Connection:

    • Factory uses sidebarTags: ["{lowercase-kind}-sidebar"]
    • Sidebar has keysAndTags["{plural}"] = []any{"{lowercase-kind}-sidebar"}
    • Sidebar ID stock-project-factory-{lowercase-kind}-details exists in targetIDs
  2. Check Navigation Flow:

    • Sidebar link → List table (CustomColumnsOverride)
    • List table Name column → Detail page (Factory)
    • All paths use consistent naming
  3. Test:

    • Verify the resource appears in the sidebar
    • Verify the list table displays correctly
    • Verify clicking a resource navigates to the detail page
    • Verify the detail page displays all relevant fields

Common Patterns

Displaying Object References

For fields that reference other resources (e.g., applicationRef, storageRef):

// In CustomColumnsOverride
createApplicationRefColumn("Application"),  // Uses helper function

// In Factory details tab
parsedText("application-ref-value", 
    "{reqsJsonPath[0]['.spec.applicationRef.kind']['-']}.{reqsJsonPath[0]['.spec.applicationRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.applicationRef.name']['-']}", 
    nil),

Displaying Timestamps

antdFlexVertical("created-block", 4, []any{
    antdText("time-label", true, "Created", nil),
    antdFlex("time-block", 6, []any{
        map[string]any{
            "type": "antdText",
            "data": map[string]any{
                "id":   "time-icon",
                "text": "🌐",
            },
        },
        map[string]any{
            "type": "parsedText",
            "data": map[string]any{
                "formatter": "timestamp",
                "id":        "time-value",
                "text":      "{reqsJsonPath[0]['.metadata.creationTimestamp']['-']}",
            },
        },
    }),
}),
antdFlexVertical("meta-namespace-block", 8, []any{
    antdText("meta-namespace-label", true, "Namespace", nil),
    antdFlex("header-row", 6, []any{
        // Badge component
        map[string]any{
            "type": "antdText",
            "data": map[string]any{
                "id":    "header-badge",
                "text":  "NS",
                "title": "namespace",
                "style": map[string]any{
                    "backgroundColor": "#a25792ff",
                    // ... badge styles
                },
            },
        },
        // Link component
        map[string]any{
            "type": "antdLink",
            "data": map[string]any{
                "id":   "namespace-link",
                "text": "{reqsJsonPath[0]['.metadata.namespace']['-']}",
                "href": "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace",
            },
        },
    }),
}),

File Reference

  • CustomColumnsOverride: internal/controller/dashboard/static_refactored.go → CreateAllCustomColumnsOverrides()
  • Factory: internal/controller/dashboard/static_refactored.go → CreateAllFactories()
  • Sidebar: internal/controller/dashboard/sidebar.go → ensureSidebar()
  • Helper Functions: internal/controller/dashboard/static_helpers.go
  • UI Helpers: internal/controller/dashboard/ui_helpers.go
  • Unified Helpers: internal/controller/dashboard/unified_helpers.go

AI Agent Prompt Template

Use this template when asking an AI agent to add a new resource to the dashboard:

Please add support for the {ResourceKind} resource ({group}/{version}/{plural}) to the Cozystack dashboard.

Resource Details:
- API Group: {group}
- Version: {version}
- Kind: {ResourceKind}
- Plural: {plural}
- Namespaced: {true/false}

Requirements:
1. Add a CustomColumnsOverride in CreateAllCustomColumnsOverrides() with:
   - ID: stock-namespace-/{group}/{version}/{plural}
   - Name column with {ResourceKind} badge linking to /factory/{lowercase-kind}-details/{name}
   - Additional columns: {list relevant columns}
   
2. Add a Factory in CreateAllFactories() with:
   - Key: {lowercase-kind}-details
   - API endpoint: /api/clusters/{2}/k8s/apis/{group}/{version}/namespaces/{3}/{plural}/{6}
   - Details tab showing all spec fields:
     {list spec fields to display}
   - Use createUnifiedFactory() approach
   
3. Add sidebar entry in ensureSidebar():
   - Add keysAndTags entry: keysAndTags["{plural}"] = []any{"{lowercase-kind}-sidebar"}
   - Add sidebar section: {specify section name or "add to existing section"}
   - Add to targetIDs: "stock-project-factory-{lowercase-kind}-details"
   
4. Ensure Factory sidebarTags matches the keysAndTags entry

Please follow the existing patterns in the codebase, particularly the Plan resource implementation as a reference.

Example: Plan Resource

The Plan resource (backups.cozystack.io/v1alpha1/plans) serves as a complete reference implementation:

  • CustomColumnsOverride: Diff in static_refactored.go
  • Factory: Diff in static_refactored.go
  • Sidebar: Diff in sidebar.go
  • Helper Function: createApplicationRefColumn() in static_helpers.go (diff)

Review this implementation for a complete working example.

Troubleshooting

Resource doesn't appear in sidebar

  • Check that keysAndTags["{plural}"] is set correctly
  • Verify the sidebar section is added to menuItems
  • Ensure the sidebar ID is in targetIDs

Clicking resource doesn't navigate to detail page

  • Verify the CustomColumnsOverride href matches the Factory key
  • Check that the Factory key is exactly {lowercase-kind}-details
  • Ensure the Factory is added to the return list in CreateAllFactories()

Detail page shows wrong sidebar

  • Verify Factory sidebarTags matches keysAndTags["{plural}"]
  • Check that the sidebar ID stock-project-factory-{lowercase-kind}-details exists
  • Ensure all sidebars are updated (they share the same keysAndTags)

Fields not displaying correctly

  • Verify JSON paths are correct (use .spec.fieldName format)
  • Check that reqsJsonPath[0] index is used for single resource views
  • Ensure field names match the actual resource spec structure

Additional Resources