feat: scale instances, --reuse-values, values diff, UI redesign, hover animations
Backend (Phase 1):
- Add ScaleInstance endpoint (POST /clusters/{id}/instances/{id}/scale)
- Add GetInstanceValuesDiff endpoint (GET .../values-diff)
- Enable ReuseValues=true in Helm Upgrade for --reuse-values behavior
- Add GetValues/GetChartDefaultValues to HelmClient interface
- Add ScaleInstanceRequest/Response and InstanceValuesDiffResponse DTOs
Frontend (Phase 2):
- InstanceCard: +/- scale buttons with loading spinner
- ModifyModal: values diff view (current vs defaults), Use Defaults button
- ArtifactBrowserPage: collapsible sidebar, compact tag grid, search filter
- TagCard: "LATEST" badge, compact layout, responsive design
- InstanceCard: compact 3-column layout, fewer scrolls needed
- InstancesManagementPage: 3-column grid, compact view
- Global hover-lift and hover-glow CSS utilities
- SidebarNav: subtle hover transition on links
This commit is contained in:
@ -206,6 +206,25 @@ type InstanceEventDiagnostics struct {
|
||||
LastTimestamp string `json:"lastTimestamp,omitempty"`
|
||||
}
|
||||
|
||||
// ScaleInstanceRequest 扩缩容实例请求
|
||||
type ScaleInstanceRequest struct {
|
||||
Replicas int `json:"replicas" binding:"required"`
|
||||
Workload string `json:"workload"`
|
||||
}
|
||||
|
||||
// ScaleInstanceResponse 扩缩容实例响应
|
||||
type ScaleInstanceResponse struct {
|
||||
Instance *InstanceResponse `json:"instance"`
|
||||
Replicas int `json:"replicas"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// InstanceValuesDiffResponse 实例 values 差异响应
|
||||
type InstanceValuesDiffResponse struct {
|
||||
Current map[string]interface{} `json:"current"`
|
||||
Defaults map[string]interface{} `json:"defaults"`
|
||||
}
|
||||
|
||||
type InstancePodLogResponse struct {
|
||||
Pod string `json:"pod"`
|
||||
Container string `json:"container"`
|
||||
|
||||
@ -371,6 +371,49 @@ func (h *InstanceHandler) StreamInstanceLogs(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
}
|
||||
|
||||
// ScaleInstance 扩缩容实例
|
||||
func (h *InstanceHandler) ScaleInstance(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
clusterID := vars["cluster_id"]
|
||||
instanceID := vars["instance_id"]
|
||||
|
||||
var req dto.ScaleInstanceRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
if req.Replicas < 0 {
|
||||
respondError(w, http.StatusBadRequest, "Invalid replicas", "replicas must be >= 0")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.instanceService.ScaleInstance(r.Context(), clusterID, instanceID, req.Replicas, req.Workload)
|
||||
if err != nil {
|
||||
respondServiceError(w, err, "Failed to scale instance")
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, dto.ScaleInstanceResponse{
|
||||
Instance: convertInstanceResponse(result, true),
|
||||
Replicas: req.Replicas,
|
||||
Message: fmt.Sprintf("Scaled to %d replicas", req.Replicas),
|
||||
})
|
||||
}
|
||||
|
||||
// GetInstanceValuesDiff 获取实例 values 差异
|
||||
func (h *InstanceHandler) GetInstanceValuesDiff(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
clusterID := vars["cluster_id"]
|
||||
instanceID := vars["instance_id"]
|
||||
|
||||
diff, err := h.instanceService.GetInstanceValuesDiff(r.Context(), clusterID, instanceID)
|
||||
if err != nil {
|
||||
respondServiceError(w, err, "Failed to get values diff")
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, diff)
|
||||
}
|
||||
|
||||
func convertInstanceEntry(entry *entity.InstanceEntry) *dto.InstanceEntryResponse {
|
||||
portResponses := make([]dto.InstanceEntryPortResponse, 0, len(entry.Ports))
|
||||
for _, port := range entry.Ports {
|
||||
|
||||
@ -194,3 +194,13 @@ func (c *HelmClientMock) GetValues(ctx context.Context, cluster *entity.Cluster,
|
||||
return instance.Values, nil
|
||||
}
|
||||
|
||||
func (c *HelmClientMock) GetChartDefaultValues(chartPath string) (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"replicaCount": 1,
|
||||
"image": map[string]interface{}{
|
||||
"repository": "nginx",
|
||||
"tag": "latest",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@ -159,6 +159,7 @@ func (h *HelmClient) Upgrade(ctx context.Context, cluster *entity.Cluster, insta
|
||||
|
||||
upgrade := action.NewUpgrade(actionConfig)
|
||||
upgrade.Namespace = instance.Namespace
|
||||
upgrade.ReuseValues = true
|
||||
upgrade.Wait = true
|
||||
upgrade.Timeout = helmOperationTimeout()
|
||||
|
||||
@ -321,6 +322,7 @@ func (h *HelmClient) GetValues(ctx context.Context, cluster *entity.Cluster, rel
|
||||
defer cleanup()
|
||||
|
||||
getValues := action.NewGetValues(actionConfig)
|
||||
getValues.AllValues = true
|
||||
values, err := getValues.Run(releaseName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get values: %w", err)
|
||||
@ -329,6 +331,21 @@ func (h *HelmClient) GetValues(ctx context.Context, cluster *entity.Cluster, rel
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// GetChartDefaultValues 从 chart 包中读取默认 values
|
||||
func (h *HelmClient) GetChartDefaultValues(chartPath string) (map[string]interface{}, error) {
|
||||
chart, err := loader.Load(chartPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load chart: %w", err)
|
||||
}
|
||||
vals := make(map[string]interface{})
|
||||
if chart.Values != nil {
|
||||
for k, v := range chart.Values {
|
||||
vals[k] = v
|
||||
}
|
||||
}
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
// convertReleaseToInstance 转换 Helm Release 为 Instance
|
||||
func (h *HelmClient) convertReleaseToInstance(rel *release.Release) *entity.Instance {
|
||||
return &entity.Instance{
|
||||
|
||||
Reference in New Issue
Block a user