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

@ -104,13 +104,41 @@ func (r *ClusterRepositoryMock) Delete(ctx context.Context, id string) error {
func (r *ClusterRepositoryMock) List(ctx context.Context) ([]*entity.Cluster, error) {
r.mu.RLock()
defer r.mu.RUnlock()
clusters := make([]*entity.Cluster, 0, len(r.clusters))
for _, cluster := range r.clusters {
// 解密敏感数据后返回
clusters = append(clusters, r.decryptCluster(cluster))
}
return clusters, nil
}
func (r *ClusterRepositoryMock) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Cluster, error) {
r.mu.RLock()
defer r.mu.RUnlock()
clusters := make([]*entity.Cluster, 0)
for _, cluster := range r.clusters {
if cluster.WorkspaceID == workspaceID {
clusters = append(clusters, r.decryptCluster(cluster))
}
}
return clusters, nil
}
func (r *ClusterRepositoryMock) GetShared(ctx context.Context) ([]*entity.Cluster, error) {
r.mu.RLock()
defer r.mu.RUnlock()
clusters := make([]*entity.Cluster, 0)
for _, cluster := range r.clusters {
if cluster.IsShared {
clusters = append(clusters, r.decryptCluster(cluster))
}
}
return clusters, nil
}

View File

@ -102,12 +102,26 @@ func (r *InstanceRepositoryMock) ListByCluster(ctx context.Context, clusterID st
func (r *InstanceRepositoryMock) List(ctx context.Context) ([]*entity.Instance, error) {
r.mu.RLock()
defer r.mu.RUnlock()
instances := make([]*entity.Instance, 0, len(r.instances))
for _, instance := range r.instances {
instances = append(instances, instance)
}
return instances, nil
}
func (r *InstanceRepositoryMock) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Instance, error) {
r.mu.RLock()
defer r.mu.RUnlock()
instances := make([]*entity.Instance, 0)
for _, instance := range r.instances {
if instance.WorkspaceID == workspaceID {
instances = append(instances, instance)
}
}
return instances, nil
}

View File

@ -0,0 +1,96 @@
package mock
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
// StorageRepositoryMock Storage 仓储 Mock
type StorageRepositoryMock struct {
storages map[string]*entity.StorageBackend
}
// NewStorageRepositoryMock 创建 Mock
func NewStorageRepositoryMock() *StorageRepositoryMock {
return &StorageRepositoryMock{
storages: make(map[string]*entity.StorageBackend),
}
}
// Create 创建存储
func (r *StorageRepositoryMock) Create(ctx context.Context, storage *entity.StorageBackend) error {
r.storages[storage.ID] = storage
return nil
}
// GetByID 获取存储
func (r *StorageRepositoryMock) GetByID(ctx context.Context, id string) (*entity.StorageBackend, error) {
if s, ok := r.storages[id]; ok {
return s, nil
}
return nil, entity.ErrStorageNotFound
}
// GetByWorkspace 获取工作空间的存储
func (r *StorageRepositoryMock) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.StorageBackend, error) {
var result []*entity.StorageBackend
for _, s := range r.storages {
if s.WorkspaceID == workspaceID {
result = append(result, s)
}
}
return result, nil
}
// GetByName 按名称获取
func (r *StorageRepositoryMock) GetByName(ctx context.Context, workspaceID, name string) (*entity.StorageBackend, error) {
for _, s := range r.storages {
if s.WorkspaceID == workspaceID && s.Name == name {
return s, nil
}
}
return nil, entity.ErrStorageNotFound
}
// Update 更新存储
func (r *StorageRepositoryMock) Update(ctx context.Context, storage *entity.StorageBackend) error {
r.storages[storage.ID] = storage
return nil
}
// Delete 删除存储
func (r *StorageRepositoryMock) Delete(ctx context.Context, id string) error {
delete(r.storages, id)
return nil
}
// GetShared 获取所有共享存储后端
func (r *StorageRepositoryMock) GetShared(ctx context.Context) ([]*entity.StorageBackend, error) {
var result []*entity.StorageBackend
for _, s := range r.storages {
if s.IsShared {
result = append(result, s)
}
}
return result, nil
}
// GetDefault 获取 workspace 的默认存储后端
func (r *StorageRepositoryMock) GetDefault(ctx context.Context, workspaceID string) (*entity.StorageBackend, error) {
for _, s := range r.storages {
if s.WorkspaceID == workspaceID && s.IsDefault {
return s, nil
}
}
return nil, entity.ErrStorageNotFound
}
// List 列出所有存储(管理员用)
func (r *StorageRepositoryMock) List(ctx context.Context) ([]*entity.StorageBackend, error) {
var result []*entity.StorageBackend
for _, s := range r.storages {
result = append(result, s)
}
return result, nil
}

View File

@ -88,12 +88,40 @@ func (r *UserRepositoryMock) Delete(ctx context.Context, id string) error {
func (r *UserRepositoryMock) List(ctx context.Context) ([]*entity.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
users := make([]*entity.User, 0, len(r.users))
for _, user := range r.users {
users = append(users, user)
}
return users, nil
}
func (r *UserRepositoryMock) ListByWorkspace(ctx context.Context, workspaceID string) ([]*entity.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
users := make([]*entity.User, 0)
for _, user := range r.users {
if user.WorkspaceID == workspaceID {
users = append(users, user)
}
}
return users, nil
}
func (r *UserRepositoryMock) ListActive(ctx context.Context) ([]*entity.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
users := make([]*entity.User, 0)
for _, user := range r.users {
if user.IsActive {
users = append(users, user)
}
}
return users, nil
}