cozystack/internal/controller/dashboard
Andrei Kvapil 57c8cc26d4
refactor(api): rename CozystackResourceDefinition to ApplicationDefinition
Rename the CRD and all related types for better clarity:
- CozystackResourceDefinition -> ApplicationDefinition
- CozystackResourceDefinitionList -> ApplicationDefinitionList
- CozystackResourceDefinitionSpec -> ApplicationDefinitionSpec
- All related nested types updated accordingly

Updated components:
- API types and generated deepcopy code
- Controllers and reconcilers
- Dashboard, lineagecontrollerwebhook, crdmem packages
- CRD YAML definition and Helm chart
- All 25 cozyrds YAML manifests
- Migration scripts and documentation

Added migration 23 to remove old cozystack-resource-definition-crd HelmRelease.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-01-15 22:35:56 +01:00
..
breadcrumb.go refactor(api): rename CozystackResourceDefinition to ApplicationDefinition 2026-01-15 22:35:56 +01:00
customcolumns.go refactor(api): rename CozystackResourceDefinition to ApplicationDefinition 2026-01-15 22:35:56 +01:00
customformsoverride.go refactor(api): rename CozystackResourceDefinition to ApplicationDefinition 2026-01-15 22:35:56 +01:00
customformsoverride_test.go [dashboard] Fix CustomFormsOverride schema to nest properties under spec.properties 2025-12-08 23:27:24 +01:00
customformsprefill.go refactor(api): rename CozystackResourceDefinition to ApplicationDefinition 2026-01-15 22:35:56 +01:00
factory.go refactor(api): rename CozystackResourceDefinition to ApplicationDefinition 2026-01-15 22:35:56 +01:00
helpers.go refactor(api): rename CozystackResourceDefinition to ApplicationDefinition 2026-01-15 22:35:56 +01:00
manager.go refactor(api): rename CozystackResourceDefinition to ApplicationDefinition 2026-01-15 22:35:56 +01:00
marketplacepanel.go refactor(api): rename CozystackResourceDefinition to ApplicationDefinition 2026-01-15 22:35:56 +01:00
README.md [backups,dashboard] Add dev guide for dashboard 2026-01-07 09:47:36 +03:00
sidebar.go refactor(api): rename CozystackResourceDefinition to ApplicationDefinition 2026-01-15 22:35:56 +01:00
static_helpers.go [backups,dashboard] User-facing UI 2026-01-07 09:47:36 +03:00
static_processor.go [dashboard] Revert reconciler removal 2025-10-30 19:37:20 +03:00
static_refactored.go [backups,dashboard] Add BackupJobs and Backups 2026-01-07 10:06:36 +03:00
tableurimapping.go refactor(api): rename CozystackResourceDefinition to ApplicationDefinition 2026-01-15 22:35:56 +01:00
ui_helpers.go [dashboard-controller] Move bages generation logic to internal dashboard 2025-11-04 17:11:31 +01:00
unified_helpers.go [dashboard-controller] Move bages generation logic to internal dashboard 2025-11-04 17:11:31 +01: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/{clusterName}/{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/{clusterName}/{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.goCreateAllCustomColumnsOverrides()
  • Factory: internal/controller/dashboard/static_refactored.goCreateAllFactories()
  • Sidebar: internal/controller/dashboard/sidebar.goensureSidebar()
  • 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