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

@ -0,0 +1,306 @@
package rest
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/service"
)
// WorkspaceHandler 工作空间 HTTP 处理程序
type WorkspaceHandler struct {
workspaceService *service.WorkspaceService
authService *service.AuthService
}
// NewWorkspaceHandler 创建工作空间处理程序
func NewWorkspaceHandler(workspaceService *service.WorkspaceService, authService *service.AuthService) *WorkspaceHandler {
return &WorkspaceHandler{
workspaceService: workspaceService,
authService: authService,
}
}
// CreateWorkspace 创建工作空间
// @Summary 创建工作空间
// @Description 创建新的工作空间Admin 专用)
// @Tags workspace
// @Accept json
// @Produce json
// @Param request body dto.CreateWorkspaceRequest true "创建工作空间请求"
// @Success 200 {object} dto.WorkspaceDTO
// @Router /workspaces [post]
func (h *WorkspaceHandler) CreateWorkspace(w http.ResponseWriter, r *http.Request) {
// 检查权限Admin
if !h.requireAdmin(w, r) {
return
}
var req dto.CreateWorkspaceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", "")
return
}
// 获取创建者 ID
userID := GetUserIDFromRequest(r)
workspace, err := h.workspaceService.Create(r.Context(), req.Name, req.Description, userID)
if err != nil {
respondError(w, http.StatusBadRequest, err.Error(), "")
return
}
respondSuccess(w, "", dto.WorkspaceDTOFromEntity(workspace))
}
// GetWorkspace 获取工作空间
// @Summary 获取工作空间
// @Description 获取指定工作空间的详细信息和配额
// @Tags workspace
// @Accept json
// @Produce json
// @Param workspace_id path string true "工作空间 ID"
// @Success 200 {object} dto.WorkspaceResponse
// @Router /workspaces/{workspace_id} [get]
func (h *WorkspaceHandler) GetWorkspace(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
workspaceID := vars["workspace_id"]
workspace, err := h.workspaceService.GetByID(r.Context(), workspaceID)
if err != nil {
respondError(w, http.StatusNotFound, "Workspace not found", "")
return
}
// 检查访问权限
if !h.canAccessWorkspace(w, r, workspace.ID) {
return
}
// 获取配额
quotas, _ := h.workspaceService.GetQuotas(r.Context(), workspace.ID)
response := dto.WorkspaceResponse{
Workspace: dto.WorkspaceDTOFromEntity(workspace),
Quotas: dto.QuotaDTOsFromEntities(quotas),
}
respondSuccess(w, "", response)
}
// UpdateWorkspace 更新工作空间
// @Summary 更新工作空间
// @Description 更新工作空间信息Admin 专用)
// @Tags workspace
// @Accept json
// @Produce json
// @Param workspace_id path string true "工作空间 ID"
// @Param request body dto.UpdateWorkspaceRequest true "更新工作空间请求"
// @Success 200 {object} dto.WorkspaceDTO
// @Router /workspaces/{workspace_id} [put]
func (h *WorkspaceHandler) UpdateWorkspace(w http.ResponseWriter, r *http.Request) {
// 检查权限Admin
if !h.requireAdmin(w, r) {
return
}
vars := mux.Vars(r)
workspaceID := vars["workspace_id"]
workspace, err := h.workspaceService.GetByID(r.Context(), workspaceID)
if err != nil {
respondError(w, http.StatusNotFound, "Workspace not found", "")
return
}
var req dto.UpdateWorkspaceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", "")
return
}
if req.Name != "" {
workspace.Name = req.Name
}
if req.Description != "" {
workspace.Description = req.Description
}
if err := h.workspaceService.Update(r.Context(), workspace); err != nil {
respondError(w, http.StatusBadRequest, err.Error(), "")
return
}
respondSuccess(w, "", dto.WorkspaceDTOFromEntity(workspace))
}
// DeleteWorkspace 删除工作空间
// @Summary 删除工作空间
// @Description 删除指定工作空间Admin 专用)
// @Tags workspace
// @Accept json
// @Produce json
// @Param workspace_id path string true "工作空间 ID"
// @Success 200
// @Router /workspaces/{workspace_id} [delete]
func (h *WorkspaceHandler) DeleteWorkspace(w http.ResponseWriter, r *http.Request) {
// 检查权限Admin
if !h.requireAdmin(w, r) {
return
}
vars := mux.Vars(r)
workspaceID := vars["workspace_id"]
if err := h.workspaceService.Delete(r.Context(), workspaceID); err != nil {
respondError(w, http.StatusBadRequest, err.Error(), "")
return
}
respondSuccess(w, "", nil)
}
// ListWorkspaces 列出所有工作空间
// @Summary 列出所有工作空间
// @Description 获取所有工作空间列表Admin 专用)
// @Tags workspace
// @Accept json
// @Produce json
// @Success 200 {object} dto.WorkspaceListResponse
// @Router /workspaces [get]
func (h *WorkspaceHandler) ListWorkspaces(w http.ResponseWriter, r *http.Request) {
// 检查权限Admin
if !h.requireAdmin(w, r) {
return
}
workspaces, err := h.workspaceService.List(r.Context())
if err != nil {
respondError(w, http.StatusInternalServerError, err.Error(), "")
return
}
respondSuccess(w, "", dto.WorkspaceListResponse{
Workspaces: dto.WorkspaceDTOsFromEntities(workspaces),
Total: len(workspaces),
})
}
// GetWorkspaceQuotas 获取工作空间配额
// @Summary 获取工作空间配额
// @Description 获取指定工作空间的资源配额
// @Tags workspace
// @Accept json
// @Produce json
// @Param workspace_id path string true "工作空间 ID"
// @Success 200 {array} dto.QuotaDTO
// @Router /workspaces/{workspace_id}/quotas [get]
func (h *WorkspaceHandler) GetWorkspaceQuotas(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
workspaceID := vars["workspace_id"]
// 检查访问权限
if !h.canAccessWorkspace(w, r, workspaceID) {
return
}
quotas, err := h.workspaceService.GetQuotas(r.Context(), workspaceID)
if err != nil {
respondError(w, http.StatusInternalServerError, err.Error(), "")
return
}
respondSuccess(w, "", dto.QuotaDTOsFromEntities(quotas))
}
// SetWorkspaceQuotas 设置工作空间配额
// @Summary 设置工作空间配额
// @Description 设置指定工作空间的 CPU/GPU/GPU Memory 配额Admin 专用)
// @Tags workspace
// @Accept json
// @Produce json
// @Param workspace_id path string true "工作空间 ID"
// @Param request body dto.SetQuotasRequest true "配额设置请求"
// @Success 200 {array} dto.QuotaDTO
// @Router /workspaces/{workspace_id}/quotas [put]
func (h *WorkspaceHandler) SetWorkspaceQuotas(w http.ResponseWriter, r *http.Request) {
// 检查权限Admin
if !h.requireAdmin(w, r) {
return
}
vars := mux.Vars(r)
workspaceID := vars["workspace_id"]
var req dto.SetQuotasRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", "")
return
}
quotas := make(map[entity.ResourceType]struct {
HardLimit float64
SoftLimit float64
})
if req.CPU != nil {
quotas[entity.ResourceCPU] = struct {
HardLimit float64
SoftLimit float64
}{req.CPU.HardLimit, req.CPU.SoftLimit}
}
if req.GPU != nil {
quotas[entity.ResourceGPU] = struct {
HardLimit float64
SoftLimit float64
}{req.GPU.HardLimit, req.GPU.SoftLimit}
}
if req.GPUMemory != nil {
quotas[entity.ResourceGPUMemory] = struct {
HardLimit float64
SoftLimit float64
}{req.GPUMemory.HardLimit, req.GPUMemory.SoftLimit}
}
if err := h.workspaceService.SetQuotas(r.Context(), workspaceID, quotas); err != nil {
respondError(w, http.StatusBadRequest, err.Error(), "")
return
}
// 返回更新后的配额
updatedQuotas, _ := h.workspaceService.GetQuotas(r.Context(), workspaceID)
respondSuccess(w, "", dto.QuotaDTOsFromEntities(updatedQuotas))
}
// requireAdmin 检查是否为 Admin
func (h *WorkspaceHandler) requireAdmin(w http.ResponseWriter, r *http.Request) bool {
userRole := r.Header.Get("X-User-Role")
if userRole != string(entity.RoleAdmin) {
respondError(w, http.StatusForbidden, "Admin access required", "")
return false
}
return true
}
// canAccessWorkspace 检查是否可以访问工作空间
func (h *WorkspaceHandler) canAccessWorkspace(w http.ResponseWriter, r *http.Request, workspaceID string) bool {
userRole := r.Header.Get("X-User-Role")
userWorkspaceID := r.Header.Get("X-Workspace-ID")
// Admin 可以访问所有
if userRole == string(entity.RoleAdmin) {
return true
}
// 普通用户只能访问自己的 workspace
if userWorkspaceID != workspaceID {
respondError(w, http.StatusForbidden, "Access denied", "")
return false
}
return true
}