cozystack/internal/controller/dashboard/README.md
Andrey Kolkov 2b1b5e8fa9 fix(dashboard): updated legacy templating
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-02-24 17:54:08 +04:00

355 lines
14 KiB
Markdown

# 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**:
```go
// 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):
```go
// 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**:
```go
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):
```go
// Add sidebar for {group} {kind} resource
keysAndTags["{plural}"] = []any{"{lowercase-kind}-sidebar"}
```
**3b. Add Sidebar Section** (if creating a new section, around line 169):
```go
// 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):
```go
"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`):
```go
// 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
```go
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
```go
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](https://github.com/cozystack/cozystack/compare/1f0b5ff9ac0d9d8896af46f8a19501c8b728671d..88da2d1f642b6cf03873d368dfdc675de23f1513#diff-8309b1db3362715b3d94a8b0beae7e95d3ccaf248d4f8702aaa12fba398da895R374-R380) in `static_refactored.go`
- **Factory**: [Diff](https://github.com/cozystack/cozystack/compare/1f0b5ff9ac0d9d8896af46f8a19501c8b728671d..88da2d1f642b6cf03873d368dfdc675de23f1513#diff-8309b1db3362715b3d94a8b0beae7e95d3ccaf248d4f8702aaa12fba398da895R1443-R1558) in `static_refactored.go`
- **Sidebar**: [Diff](https://github.com/cozystack/cozystack/compare/1f0b5ff9ac0d9d8896af46f8a19501c8b728671d..88da2d1f642b6cf03873d368dfdc675de23f1513#diff-be79027f7179e457a8f10e225bb921a197ffa390eb8f916d8d21379fadd54a56) in `sidebar.go`
- **Helper Function**: `createApplicationRefColumn()` in `static_helpers.go` ([diff](https://github.com/cozystack/cozystack/compare/1f0b5ff9ac0d9d8896af46f8a19501c8b728671d..88da2d1f642b6cf03873d368dfdc675de23f1513#diff-f17bcccc089cac3a8e965b13b9ab26e678d45bfc9a58d842399f218703e06a08R1026-R1046))
Review [this implementation](https://github.com/cozystack/cozystack/compare/1f0b5ff9ac0d9d8896af46f8a19501c8b728671d..88da2d1f642b6cf03873d368dfdc675de23f1513) 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
- 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)