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> |
||
|---|---|---|
| .. | ||
| breadcrumb.go | ||
| customcolumns.go | ||
| customformsoverride.go | ||
| customformsoverride_test.go | ||
| customformsprefill.go | ||
| factory.go | ||
| helpers.go | ||
| manager.go | ||
| marketplacepanel.go | ||
| navigation.go | ||
| README.md | ||
| sidebar.go | ||
| static_helpers.go | ||
| static_processor.go | ||
| static_refactored.go | ||
| tableurimapping.go | ||
| ui_helpers.go | ||
| unified_helpers.go | ||
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 valuescreateTimestampColumn(): Timestamp fieldscreateReadyColumn(): Ready status from conditions- Custom helpers for complex fields
Helper Functions Available:
createCustomColumnWithJsonPath(name, jsonPath, badgeValue, badgeColor, linkHref): Column with badge and linkcreateStringColumn(name, jsonPath): Simple string columncreateTimestampColumn(name, jsonPath): Timestamp with formattingcreateReadyColumn(): Ready status columncreateBoolColumn(name, jsonPath): Boolean columncreateArrayColumn(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-detailsmatches/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"]bycreateUnifiedFactory() - Tabs: Define the detail page content (Details, YAML, etc.)
UI Helper Functions:
contentCard(id, style, children): Container cardantdText(id, strong, text, style): Text elementantdFlexVertical(id, gap, children): Vertical flex containerantdRow(id, gutter, children): Row layoutantdCol(id, span, children): Column layoutparsedText(id, text, style): Text with JSON path parsingparsedTextWithFormatter(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
keysAndTagsandmenuItems, 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}-detailsexists intargetIDs
- Factory uses
-
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()instatic_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
sidebarTagsmatcheskeysAndTags["{plural}"] - Check that the sidebar ID
stock-project-factory-{lowercase-kind}-detailsexists - Ensure all sidebars are updated (they share the same
keysAndTags)
Fields not displaying correctly
- Verify JSON paths are correct (use
.spec.fieldNameformat) - Check that
reqsJsonPath[0]index is used for single resource views - Ensure field names match the actual resource spec structure
Additional Resources
- Kubernetes API Conventions: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md
- Dashboard API Types:
api/dashboard/v1alpha1/ - Resource Types:
api/backups/v1alpha1/(example)