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

@ -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")
}