355 lines
14 KiB
Markdown
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)
|
|
|