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:
- CustomColumnsOverride: Defines how the resource appears in list/table views
- Factory: Defines the detail page layout for individual resource instances
- 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),
}),
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
-
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
-
Check Navigation Flow:
- Sidebar link → List table (CustomColumnsOverride)
- List table Name column → Detail page (Factory)
- All paths use consistent naming
-
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']['-']}",
},
},
}),
}),
Displaying Namespace with Link
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
- 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()
- 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