cozystack/internal/controller/dashboard
Myasnikov Daniil 6105882856
fix(dashboard): address review feedback on RestoreJob views
- Replace rename-only wording on spec.targetApplicationRef form labels so
  users discover that the field also supports restoring into a different
  application, not just renaming.
- Drop VMInstance-specific keys from the spec.options form label; the
  restore options are driver-specific, so hardcoding one driver's keys is
  misleading once other drivers land.
- Render a single "Same as backup" fallback for an omitted
  spec.targetApplicationRef on the details page instead of the noisy
  "-.-/-" produced by concatenating three missing path fallbacks.
- De-duplicate the time-block / time-icon / time-value component IDs
  within the restorejob-details factory by prefixing them with
  created-, started-at-, and completed-at-.
- Mark non-CRD-backed stock-project-factory-*-details sidebars (kube-*,
  plan, backupjob, backup, restorejob) as static in
  upsertMultipleSidebars so they pick up consistent managed-by labels
  instead of being left unmanaged.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-04-21 17:27:30 +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 [dashboard] Guard nested type assertions in VMDisk customformsoverride 2026-04-14 19:59:19 +05:00
customformsoverride_test.go [vm-default-images] Added new optional package 2026-04-14 19:59:19 +05:00
customformsprefill.go refactor(api): rename CozystackResourceDefinition to ApplicationDefinition 2026-01-15 22:35:56 +01:00
factory.go feat(application): add WorkloadsReady condition and Events tab 2026-04-15 17:20:45 +03:00
factory_test.go feat(application): add WorkloadsReady condition and Events tab 2026-04-15 17:20:45 +03: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): address review feedback on RestoreJob views 2026-04-21 17:27:30 +05:00
static_helpers.go fix(dashboard): fall back to .firstTimestamp in Event Time column 2026-04-16 01:10:10 +03:00
static_processor.go [dashboard] Upgrade dashboard to version 1.4.0 2026-02-15 19:49:19 +05:00
static_refactored.go fix(dashboard): address review feedback on RestoreJob views 2026-04-21 17:27:30 +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