cozystack/internal/controller/dashboard
Kirill Ilin c27807952a
[vm-instance] Rename subnets to networks and add dropdown selector
Rename the misleading 'subnets' field to 'networks' in VMInstance to
better reflect that it attaches the VM to VPC network attachments, not
creates subnets. The old 'subnets' field is kept as deprecated with
fallback logic for backward compatibility.

Add an API-backed dropdown (listInput) for networks[].name that lists
available NetworkAttachmentDefinitions in the namespace, and hide the
deprecated subnets field from the dashboard UI.

Assisted-By: Claude AI
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-30 10:53:47 +05:00
..
breadcrumb.go [dashboard] Upgrade dashboard to version 1.4.0 2026-02-15 19:49:19 +05:00
customcolumns.go [dashboard] Add resource quota usage to tenant info resource 2026-01-29 14:15:02 +05:00
customformsoverride.go [vm-instance] Rename subnets to networks and add dropdown selector 2026-03-30 10:53:47 +05:00
customformsoverride_test.go [vm-instance] Rename subnets to networks and add dropdown selector 2026-03-30 10:53:47 +05:00
customformsprefill.go refactor(api): rename CozystackResourceDefinition to ApplicationDefinition 2026-01-15 22:35:56 +01:00
factory.go fix(dashboard): check only Include selectors for tab visibility 2026-02-23 20:20:03 +01:00
helpers.go refactor(api): rename CozystackResourceDefinition to ApplicationDefinition 2026-01-15 22:35:56 +01:00
manager.go fix(dashboard): restore stock-instance sidebars for namespace-level pages 2026-03-02 23:13:48 +05:00
marketplacepanel.go fix(dashboard): preserve disabled/hidden state on MarketplacePanel reconciliation 2026-03-10 12:41:43 +01:00
navigation.go [dashboard] Upgrade dashboard to version 1.4.0 2026-02-15 19:49:19 +05:00
README.md fix(dashboard): updated legacy templating 2026-02-24 17:54:08 +04:00
sidebar.go fix(dashboard): exclude hidden MarketplacePanel resources from sidebar menu 2026-03-10 12:41:42 +01:00
static_helpers.go [dashboard] Upgrade dashboard to version 1.4.0 2026-02-15 19:49:19 +05:00
static_processor.go [dashboard] Upgrade dashboard to version 1.4.0 2026-02-15 19:49:19 +05:00
static_refactored.go [dashboard] Fix EndpointSlice column definitions for Pod serving table (#2266) 2026-03-27 17:48:40 +05:00
tableurimapping.go refactor(api): rename CozystackResourceDefinition to ApplicationDefinition 2026-01-15 22:35:56 +01:00
ui_helpers.go [dashboard] Add "Edit" button to all resources 2026-01-28 12:27:13 +05:00
unified_helpers.go [dashboard] Add "Edit" button to all resources 2026-01-28 12:27:13 +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.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