feat(frontend): add Helm chart browser, monitoring, chart-references and values templates pages

Add new frontend pages for the multi-tenant OCDP platform:

- Charts page (/charts): Browse Harbor OCI registries to list Helm chart repositories
  and versions, with deploy modal to launch charts on selected clusters
- Monitoring page (/monitoring): Display cluster metrics (CPU/Memory/GPU usage)
  and per-node details with resource utilization bars
- Chart References page (/chart-references): CRUD for chart metadata references
- Values Templates page (/templates): CRUD for Helm values templates with version
  history and rollback support
- Sidebar: Add Charts navigation, update Storage and Templates links
- api.ts: Add all API client functions (clusterApi, registryApi, instanceApi,
  monitoringApi, storageApi, chartReferenceApi, valuesTemplateApi,
  workspaceApi, userApi) with full TypeScript types

Note: deploy flow and values template rollback not yet end-to-end tested.
This commit is contained in:
Ivan087
2026-04-15 16:59:31 +08:00
parent c5e51ed069
commit 29d0310f03
283 changed files with 24658 additions and 36038 deletions

View File

@ -42,3 +42,8 @@ type ValuesSchemaResponse struct {
Schema string `json:"schema"`
}
// ValuesResponse Values 响应
type ValuesResponse struct {
Values string `json:"values"`
}

View File

@ -0,0 +1,31 @@
package dto
// CreateChartReferenceRequest 创建 Chart 引用请求
type CreateChartReferenceRequest struct {
RegistryID string `json:"registry_id" binding:"required"`
Repository string `json:"repository" binding:"required"`
ChartName string `json:"chart_name" binding:"required"`
Description string `json:"description"`
}
// UpdateChartReferenceRequest 更新 Chart 引用请求
type UpdateChartReferenceRequest struct {
RegistryID string `json:"registry_id"`
Repository string `json:"repository"`
ChartName string `json:"chart_name"`
Description string `json:"description"`
IsEnabled *bool `json:"is_enabled"`
}
// ChartReferenceResponse Chart 引用响应
type ChartReferenceResponse struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id,omitempty"`
RegistryID string `json:"registry_id"`
Repository string `json:"repository"`
ChartName string `json:"chart_name"`
Description string `json:"description"`
IsEnabled bool `json:"is_enabled"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}

View File

@ -2,30 +2,36 @@ package dto
// CreateClusterRequest 创建集群请求
type CreateClusterRequest struct {
Name string `json:"name" binding:"required"`
Host string `json:"host" binding:"required"`
CAData string `json:"caData"`
CADataAlt string `json:"ca_data"`
CertData string `json:"certData"`
CertDataAlt string `json:"cert_data"`
KeyData string `json:"keyData"`
KeyDataAlt string `json:"key_data"`
Token string `json:"token"`
Description string `json:"description"`
Name string `json:"name" binding:"required"`
Host string `json:"host" binding:"required"`
CAData string `json:"caData"`
CADataAlt string `json:"ca_data"`
CertData string `json:"certData"`
CertDataAlt string `json:"cert_data"`
KeyData string `json:"keyData"`
KeyDataAlt string `json:"key_data"`
Token string `json:"token"`
Description string `json:"description"`
IsolationMode string `json:"isolationMode"` // 'namespace' | 'cluster'
DefaultNamespace string `json:"defaultNamespace"` // 默认 namespace 前缀
IsShared bool `json:"isShared"` // 是否为共享集群
}
// UpdateClusterRequest 更新集群请求
type UpdateClusterRequest struct {
Name string `json:"name"`
Host string `json:"host"`
CAData string `json:"caData"`
CADataAlt string `json:"ca_data"`
CertData string `json:"certData"`
CertDataAlt string `json:"cert_data"`
KeyData string `json:"keyData"`
KeyDataAlt string `json:"key_data"`
Token string `json:"token"`
Description string `json:"description"`
Name string `json:"name"`
Host string `json:"host"`
CAData string `json:"caData"`
CADataAlt string `json:"ca_data"`
CertData string `json:"certData"`
CertDataAlt string `json:"cert_data"`
KeyData string `json:"keyData"`
KeyDataAlt string `json:"key_data"`
Token string `json:"token"`
Description string `json:"description"`
IsolationMode string `json:"isolationMode"`
DefaultNamespace string `json:"defaultNamespace"`
IsShared *bool `json:"isShared"`
}
// Normalize 将多种命名风格的字段合并到统一字段
@ -56,10 +62,16 @@ func (r *UpdateClusterRequest) Normalize() {
// ClusterResponse 集群响应(敏感数据已脱敏)
type ClusterResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Host string `json:"host"`
Description string `json:"description"`
ID string `json:"id"`
WorkspaceID string `json:"workspaceId,omitempty"`
OwnerID string `json:"ownerId,omitempty"`
Name string `json:"name"`
Host string `json:"host"`
Description string `json:"description"`
IsolationMode string `json:"isolationMode"` // 'namespace' | 'cluster'
DefaultNamespace string `json:"defaultNamespace"` // 默认 namespace 前缀
IsShared bool `json:"isShared"` // 是否为共享集群
// 认证配置状态(不返回实际证书数据,仅返回是否已配置)
HasCAData bool `json:"hasCaData"`
HasCertData bool `json:"hasCertData"`

View File

@ -1,6 +1,8 @@
package dto
import (
"time"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/pkg/crypto"
)
@ -8,42 +10,50 @@ import (
// ToRegistryResponse 转换 Registry 实体为响应 DTO脱敏
func ToRegistryResponse(registry *entity.Registry) *RegistryResponse {
response := &RegistryResponse{
ID: registry.ID,
Name: registry.Name,
URL: registry.URL,
Description: registry.Description,
Username: registry.Username,
Insecure: registry.Insecure,
CreatedAt: registry.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: registry.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
ID: registry.ID,
WorkspaceID: registry.WorkspaceID,
OwnerID: registry.OwnerID,
Name: registry.Name,
URL: registry.URL,
Description: registry.Description,
Username: registry.Username,
Insecure: registry.Insecure,
IsShared: registry.IsShared,
CreatedAt: registry.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: registry.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
// 脱敏处理密码
if registry.Password != "" {
response.HasPassword = true
response.Password = crypto.MaskSensitiveData(registry.Password)
}
return response
}
// ToClusterResponse 转换 Cluster 实体为响应 DTO脱敏
func ToClusterResponse(cluster *entity.Cluster) *ClusterResponse {
response := &ClusterResponse{
ID: cluster.ID,
Name: cluster.Name,
Host: cluster.Host,
Description: cluster.Description,
CreatedAt: cluster.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: cluster.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
ID: cluster.ID,
WorkspaceID: cluster.WorkspaceID,
OwnerID: cluster.OwnerID,
Name: cluster.Name,
Host: cluster.Host,
Description: cluster.Description,
IsolationMode: string(cluster.IsolationMode),
DefaultNamespace: cluster.DefaultNamespace,
IsShared: cluster.IsShared,
CreatedAt: cluster.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: cluster.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
// 设置认证配置状态标志
response.HasCAData = cluster.CAData != ""
response.HasCertData = cluster.CertData != ""
response.HasKeyData = cluster.KeyData != ""
response.HasToken = cluster.Token != ""
// 脱敏处理敏感数据(仅显示掩码)
if cluster.CAData != "" {
response.CAData = crypto.MaskSensitiveData(cluster.CAData)
@ -57,7 +67,86 @@ func ToClusterResponse(cluster *entity.Cluster) *ClusterResponse {
if cluster.Token != "" {
response.Token = crypto.MaskSensitiveData(cluster.Token)
}
return response
}
// WorkspaceDTOFromEntity 转换 Workspace 实体为 DTO
func WorkspaceDTOFromEntity(workspace *entity.Workspace) *WorkspaceDTO {
return &WorkspaceDTO{
ID: workspace.ID,
Name: workspace.Name,
Description: workspace.Description,
CreatedBy: workspace.CreatedBy,
CreatedAt: workspace.CreatedAt,
UpdatedAt: workspace.UpdatedAt,
}
}
// WorkspaceDTOsFromEntities 批量转换
func WorkspaceDTOsFromEntities(workspaces []*entity.Workspace) []*WorkspaceDTO {
result := make([]*WorkspaceDTO, len(workspaces))
for i, w := range workspaces {
result[i] = WorkspaceDTOFromEntity(w)
}
return result
}
// QuotaDTOFromEntity 转换 Quota 实体为 DTO
func QuotaDTOFromEntity(quota *entity.WorkspaceQuota) *QuotaDTO {
return &QuotaDTO{
ID: quota.ID,
WorkspaceID: quota.WorkspaceID,
ResourceType: string(quota.ResourceType),
HardLimit: quota.HardLimit,
SoftLimit: quota.SoftLimit,
Used: quota.Used,
}
}
// QuotaDTOsFromEntities 批量转换
func QuotaDTOsFromEntities(quotas []*entity.WorkspaceQuota) []*QuotaDTO {
result := make([]*QuotaDTO, len(quotas))
for i, q := range quotas {
result[i] = QuotaDTOFromEntity(q)
}
return result
}
// UserDTOFromEntity 转换 User 实体为 DTO
func UserDTOFromEntity(user *entity.User, workspaceName string) *UserDTO {
return &UserDTO{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Role: string(user.Role),
WorkspaceID: user.WorkspaceID,
WorkspaceName: workspaceName,
IsActive: user.IsActive,
MustChangePassword: user.MustChangePassword,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
}
// UserDTOsFromEntities 批量转换
func UserDTOsFromEntities(users []*entity.User, workspaceNames map[string]string) []*UserDTO {
result := make([]*UserDTO, len(users))
for i, u := range users {
workspaceName := ""
if u.WorkspaceID != "" {
workspaceName = workspaceNames[u.WorkspaceID]
}
result[i] = UserDTOFromEntity(u, workspaceName)
}
return result
}
// TimeToString 转换时间
func TimeToString(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format("2006-01-02T15:04:05Z07:00")
}

View File

@ -23,12 +23,15 @@ type UpdateRegistryRequest struct {
// RegistryResponse Registry 响应(敏感数据已脱敏)
type RegistryResponse struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id,omitempty"`
OwnerID string `json:"owner_id,omitempty"`
Name string `json:"name"`
URL string `json:"url"`
Description string `json:"description"`
Username string `json:"username,omitempty"` // 明文返回用户名(不敏感)
Password string `json:"password,omitempty"` // 脱敏显示(••••••••)
HasPassword bool `json:"hasPassword"` // 是否已设置密码
IsShared bool `json:"is_shared"`
Insecure bool `json:"insecure"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`

View File

@ -0,0 +1,73 @@
package dto
// CreateStorageRequest 创建存储后端请求
type CreateStorageRequest struct {
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required"` // nfs, pv, hostPath
Description string `json:"description"`
IsDefault bool `json:"is_default"`
IsShared bool `json:"is_shared"`
// NFS 配置
NFS NFSConfigDTO `json:"nfs,omitempty"`
// PV 配置
PV PVConfigDTO `json:"pv,omitempty"`
// HostPath 配置
HostPath HostPathConfigDTO `json:"hostPath,omitempty"`
}
// UpdateStorageRequest 更新存储后端请求
type UpdateStorageRequest struct {
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
IsDefault bool `json:"is_default"`
IsShared bool `json:"is_shared"`
// NFS 配置
NFS NFSConfigDTO `json:"nfs,omitempty"`
// PV 配置
PV PVConfigDTO `json:"pv,omitempty"`
// HostPath 配置
HostPath HostPathConfigDTO `json:"hostPath,omitempty"`
}
// NFSConfigDTO NFS 配置
type NFSConfigDTO struct {
Server string `json:"server"`
Path string `json:"path"`
}
// PVConfigDTO PV 配置
type PVConfigDTO struct {
StorageClassName string `json:"storageClassName"`
Capacity string `json:"capacity"`
AccessModes []string `json:"accessModes"`
}
// HostPathConfigDTO HostPath 配置
type HostPathConfigDTO struct {
Path string `json:"path"`
}
// StorageResponse 存储后端响应
type StorageResponse struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id,omitempty"`
OwnerID string `json:"owner_id,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Config StorageConfigDTO `json:"config"`
Description string `json:"description"`
IsDefault bool `json:"is_default"`
IsShared bool `json:"is_shared"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
// StorageConfigDTO 存储配置(脱敏后)
type StorageConfigDTO struct {
NFS *NFSConfigDTO `json:"nfs,omitempty"`
PV *PVConfigDTO `json:"pv,omitempty"`
HostPath *HostPathConfigDTO `json:"hostPath,omitempty"`
}

View File

@ -0,0 +1,78 @@
package dto
import "time"
// UserDTO 用户 DTO
type UserDTO struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email,omitempty"`
Role string `json:"role"`
WorkspaceID string `json:"workspace_id,omitempty"`
WorkspaceName string `json:"workspace_name,omitempty"`
IsActive bool `json:"is_active"`
MustChangePassword bool `json:"must_change_password"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CreateUserRequest 创建用户请求Admin 操作)
type CreateUserRequest struct {
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required,min=6"`
Email string `json:"email"`
Role string `json:"role" validate:"required,oneof=admin user"`
WorkspaceID string `json:"workspace_id"`
}
// UpdateUserRequest 更新用户请求
type UpdateUserRequest struct {
Email string `json:"email"`
IsActive *bool `json:"is_active"`
}
// ChangeUserWorkspaceRequest 分配用户到 Workspace 请求
type ChangeUserWorkspaceRequest struct {
WorkspaceID string `json:"workspace_id" validate:"required"`
}
// ResetPasswordRequest 重置密码请求
type ResetPasswordRequest struct {
NewPassword string `json:"new_password" validate:"required,min=6"`
}
// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" validate:"required"`
NewPassword string `json:"new_password" validate:"required,min=6"`
}
// SetUserActiveRequest 启用/禁用用户请求
type SetUserActiveRequest struct {
IsActive bool `json:"is_active"`
}
// UserListResponse 用户列表响应
type UserListResponse struct {
Users []*UserDTO `json:"users"`
Total int `json:"total"`
}
// UserWithWorkspaceResponse 用户及其 Workspace 响应
type UserWithWorkspaceResponse struct {
User *UserDTO `json:"user"`
Workspace *WorkspaceDTO `json:"workspace,omitempty"`
}
// LoginResponse 登录响应
type LoginResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
MustChangePassword bool `json:"must_change_password"`
}
// UserResponseWithDTO 用户响应包含完整DTO
type UserResponseWithDTO struct {
User *UserDTO `json:"user"`
}

View File

@ -0,0 +1,37 @@
package dto
// CreateValuesTemplateRequest 创建 Values 模板请求
type CreateValuesTemplateRequest struct {
ChartReferenceID string `json:"chart_reference_id" binding:"required"`
Name string `json:"name" binding:"required"`
Description string `json:"description"`
ValuesYAML string `json:"values_yaml" binding:"required"`
IsDefault bool `json:"is_default"`
}
// UpdateValuesTemplateRequest 更新 Values 模板请求
type UpdateValuesTemplateRequest struct {
Description string `json:"description"`
ValuesYAML string `json:"values_yaml"`
IsDefault *bool `json:"is_default"`
}
// ValuesTemplateResponse Values 模板响应
type ValuesTemplateResponse struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id,omitempty"`
OwnerID string `json:"owner_id,omitempty"`
ChartReferenceID string `json:"chart_reference_id"`
Name string `json:"name"`
Description string `json:"description"`
ValuesYAML string `json:"values_yaml"`
Version int `json:"version"`
IsDefault bool `json:"is_default"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
// RollbackValuesTemplateRequest 回滚请求
type RollbackValuesTemplateRequest struct {
TemplateID string `json:"template_id" binding:"required"`
}

View File

@ -0,0 +1,67 @@
package dto
import "time"
// WorkspaceDTO 工作空间 DTO
type WorkspaceDTO struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CreateWorkspaceRequest 创建工作空间请求
type CreateWorkspaceRequest struct {
Name string `json:"name" validate:"required"`
Description string `json:"description"`
}
// UpdateWorkspaceRequest 更新工作空间请求
type UpdateWorkspaceRequest struct {
Name string `json:"name"`
Description string `json:"description"`
}
// QuotaDTO 配额 DTO
type QuotaDTO struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
ResourceType string `json:"resource_type"`
HardLimit float64 `json:"hard_limit"`
SoftLimit float64 `json:"soft_limit"`
Used float64 `json:"used"`
}
// SetQuotaRequest 设置配额请求
type SetQuotaRequest struct {
ResourceType string `json:"resource_type" validate:"required"`
HardLimit float64 `json:"hard_limit" validate:"required"`
SoftLimit float64 `json:"soft_limit"`
}
// SetQuotasRequest 批量设置配额请求
type SetQuotasRequest struct {
CPU *QuotaValue `json:"cpu"`
GPU *QuotaValue `json:"gpu"`
GPUMemory *QuotaValue `json:"gpu_memory"`
}
// QuotaValue 配额值
type QuotaValue struct {
HardLimit float64 `json:"hard_limit"`
SoftLimit float64 `json:"soft_limit"`
}
// WorkspaceResponse 工作空间响应
type WorkspaceResponse struct {
Workspace *WorkspaceDTO `json:"workspace"`
Quotas []*QuotaDTO `json:"quotas,omitempty"`
}
// WorkspaceListResponse 工作空间列表响应
type WorkspaceListResponse struct {
Workspaces []*WorkspaceDTO `json:"workspaces"`
Total int `json:"total"`
}