修改了应用商店的详情页面

This commit is contained in:
panshuxiao
2025-09-02 11:45:50 +08:00
repo.diff.parent c1c6ae5351
repo.diff.commit d86772249c
repo.diff.stats_desc%!(EXTRA int=9, int=1053, int=102)

repo.diff.view_file

@@ -212,6 +212,8 @@ func UpdateApplication(ctx context.Context, client dynamic_client.Interface, opt
// 更新资源
result, err := client.Resource(applicationGroupVersionResource).
Namespace(opts.Namespace).Update(ctx, unstructuredObj, opts.UpdateOptions)
log.Info("Updated Application: %v", result)
log.Info("Error: %v", err)
if err != nil {
return nil, application_errors.ErrOperateApplication{
Action: "update Application via Dynamic Client",

repo.diff.view_file

@@ -52,7 +52,7 @@ func handleGetApps(ctx *context.Context) {
category := ctx.FormString("category")
tag := ctx.FormString("tag")
search := ctx.FormString("search")
deployment := ctx.FormString("deployment") // docker, kubernetes, all
deployment := ctx.FormString("deployment") // docker, kubernetes
source := ctx.FormString("source") // local | devstar
manager := appstore.NewManager(ctx)
@@ -82,7 +82,7 @@ func handleGetApps(ctx *context.Context) {
continue
}
}
if deployment != "" && deployment != "all" {
if deployment != "" {
// 处理部署类型过滤,包括 'both' 类型
appDeployment := a.DeploymentType
if appDeployment == "" {
@@ -139,8 +139,8 @@ func handleGetApps(ctx *context.Context) {
}
// 处理部署类型过滤,包括 'both' 类型
matchDeployment := deployment == "" || deployment == "all"
if deployment != "" && deployment != "all" {
matchDeployment := deployment == ""
if deployment != "" {
appDeployment := app.DeploymentType
if appDeployment == "" {
// 如果没有 deployment_type 字段,尝试从其他字段获取
@@ -194,7 +194,7 @@ func handleGetCategories(ctx *context.Context) {
func handleGetTags(ctx *context.Context) {
tags, err := appstore_model.GetTags(ctx)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
ctx.JSON(http.StatusBadRequest, map[string]interface{}{
"error": err.Error(),
})
return
@@ -205,8 +205,42 @@ func handleGetTags(ctx *context.Context) {
})
}
// AppStoreStatus handles app instance status requests
func AppStoreStatus(ctx *context.Context) {
appID := ctx.PathParam("app_id")
if appID == "" {
ctx.JSON(http.StatusBadRequest, map[string]interface{}{
"error": "应用ID不能为空",
})
return
}
manager := appstore.NewManager(ctx)
status, err := manager.GetAppStatus(appID)
if err != nil {
if appErr, ok := err.(*appstore.AppStoreError); ok {
ctx.JSON(http.StatusNotFound, map[string]interface{}{
"error": appErr.Message,
})
} else {
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"error": "获取应用状态失败",
})
}
return
}
ctx.JSON(http.StatusOK, status)
}
// AppStoreInstall handles app installation
func AppStoreInstall(ctx *context.Context) {
// 检查用户登录状态
if !ctx.IsSigned {
ctx.JSON(401, map[string]string{"error": "未登录"})
return
}
if ctx.Req.Method != "POST" {
ctx.JSON(405, map[string]string{"error": "Method Not Allowed"})
return
@@ -266,8 +300,81 @@ func AppStoreInstall(ctx *context.Context) {
}
}
// AppStoreUpdate handles app update
func AppStoreUpdate(ctx *context.Context) {
// 检查用户登录状态
if !ctx.IsSigned {
ctx.JSON(401, map[string]string{"error": "未登录"})
return
}
if ctx.Req.Method != "POST" {
ctx.JSON(405, map[string]string{"error": "Method Not Allowed"})
return
}
// 解析表单数据
appID := ctx.FormString("app_id")
configJSON := ctx.FormString("config")
installTarget := ctx.FormString("install_target")
kubeconfig := ctx.FormString("kubeconfig")
kubeconfigContext := ctx.FormString("kubeconfig_context")
if appID == "" {
ctx.JSON(400, map[string]string{"error": "应用ID不能为空"})
return
}
// 权限校验:仅管理员可选择默认位置
if installTarget == "local" && (ctx.Doer == nil || !ctx.Doer.IsAdmin) {
ctx.JSON(403, map[string]string{"error": "仅管理员可选择默认位置"})
return
}
// 创建 manager 并执行更新
manager := appstore.NewManager(ctx)
if err := manager.UpdateInstalledApp(appID, configJSON, installTarget, kubeconfig, kubeconfigContext); err != nil {
// 根据错误类型返回相应的状态码和消息
if appErr, ok := err.(*appstore.AppStoreError); ok {
switch appErr.Code {
case "APP_NOT_FOUND", "CONFIG_MERGE_ERROR":
ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details})
case "KUBERNETES_UPDATE_ERROR":
ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details})
case "NOT_IMPLEMENTED":
ctx.JSON(501, map[string]string{"error": appErr.Message})
default:
ctx.JSON(500, map[string]string{"error": appErr.Message})
}
} else {
ctx.JSON(500, map[string]string{"error": "更新失败: " + err.Error()})
}
return
}
// 更新成功
if installTarget == "kubeconfig" && kubeconfig != "" {
ctx.Flash.Success("应用已成功更新到自定义位置")
} else {
ctx.Flash.Success("应用已成功更新到默认位置")
}
// 优先根据表单的 redirect_to 回跳(前端会带上当前路径)
if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" {
ctx.RedirectToCurrentSite(redirectTo)
} else {
ctx.Redirect(setting.AppSubURL + "/user/settings/appstore")
}
}
// AppStoreUninstall handles app uninstallation
func AppStoreUninstall(ctx *context.Context) {
// 检查用户登录状态
if !ctx.IsSigned {
ctx.JSON(401, map[string]string{"error": "未登录"})
return
}
if ctx.Req.Method != "POST" {
ctx.JSON(405, map[string]string{"error": "Method Not Allowed"})
return
@@ -321,6 +428,67 @@ func AppStoreUninstall(ctx *context.Context) {
}
}
// AppStoreStop handles app stopping (setting replicas to 0)
func AppStoreStop(ctx *context.Context) {
// 检查用户登录状态
if !ctx.IsSigned {
ctx.JSON(401, map[string]string{"error": "未登录"})
return
}
if ctx.Req.Method != "POST" {
ctx.JSON(405, map[string]string{"error": "Method Not Allowed"})
return
}
// 解析表单数据
appID := ctx.FormString("app_id")
installTarget := ctx.FormString("install_target")
kubeconfig := ctx.FormString("kubeconfig")
kubeconfigContext := ctx.FormString("kubeconfig_context")
if appID == "" {
ctx.JSON(400, map[string]string{"error": "应用ID不能为空"})
return
}
// 创建 manager 并执行暂停
manager := appstore.NewManager(ctx)
if err := manager.StopApp(appID, installTarget, []byte(kubeconfig), kubeconfigContext); err != nil {
// 根据错误类型返回相应的状态码和消息
if appErr, ok := err.(*appstore.AppStoreError); ok {
switch appErr.Code {
case "APP_NOT_FOUND":
ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details})
case "KUBERNETES_STOP_ERROR":
ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details})
case "UNSUPPORTED_DEPLOYMENT_TYPE":
ctx.JSON(400, map[string]string{"error": appErr.Message})
default:
ctx.JSON(500, map[string]string{"error": appErr.Message})
}
} else {
ctx.JSON(500, map[string]string{"error": "暂停失败: " + err.Error()})
}
return
}
// 暂停成功
if installTarget == "kubeconfig" && kubeconfig != "" {
ctx.Flash.Success("应用已成功在指定位置暂停")
} else {
ctx.Flash.Success("应用已成功暂停")
}
// 根据来源页面决定重定向位置
referer := ctx.Req.Header.Get("Referer")
if strings.Contains(referer, "/admin/appstore") {
ctx.Redirect(setting.AppSubURL + "/admin/appstore")
} else {
ctx.Redirect(setting.AppSubURL + "/user/settings/appstore")
}
}
// AppStoreConfigure handles app configuration
func AppStoreConfigure(ctx *context.Context) {
// TODO: Load app configuration form
@@ -332,6 +500,12 @@ func AppStoreConfigure(ctx *context.Context) {
// AppStoreConfigurePost handles app configuration form submission
func AppStoreConfigurePost(ctx *context.Context) {
// 检查用户登录状态
if !ctx.IsSigned {
ctx.JSON(401, map[string]string{"error": "未登录"})
return
}
// TODO: Handle configuration form submission
ctx.Flash.Success("App configuration feature coming soon")
if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" {
@@ -343,6 +517,12 @@ func AppStoreConfigurePost(ctx *context.Context) {
// 添加应用API
func AddAppAPI(ctx *context.Context) {
// 检查 CSRF token
if !ctx.IsSigned {
ctx.JSON(401, map[string]string{"error": "未登录"})
return
}
manager := appstore.NewManager(ctx)
if ctx.Req.Method != "POST" {
ctx.JSON(405, map[string]string{"error": "Method Not Allowed"})

repo.diff.view_file

@@ -706,8 +706,11 @@ func registerWebRoutes(m *web.Router) {
m.Group("/appstore", func() {
m.Get("", user_setting.AppStore)
m.Get("/api/{action}", user_setting.AppStoreAPI)
m.Get("/api/status/{app_id}", user_setting.AppStoreStatus)
m.Post("/api/add", user_setting.AddAppAPI)
m.Post("/install/{app_id}", user_setting.AppStoreInstall)
m.Post("/update/{app_id}", user_setting.AppStoreUpdate)
m.Post("/stop/{app_id}", user_setting.AppStoreStop)
m.Post("/uninstall/{app_id}", user_setting.AppStoreUninstall)
m.Get("/configure/{app_id}", user_setting.AppStoreConfigure)
m.Post("/configure/{app_id}", user_setting.AppStoreConfigurePost)

repo.diff.view_file

@@ -1,6 +1,6 @@
{
"id": "nginx",
"name": "Nginx Web Server",
"id": "nginx-9",
"name": "Nginx Web Server-9",
"description": "High-performance HTTP server and reverse proxy",
"category": "web-server",
"tags": ["web", "proxy", "http", "server"],
@@ -56,7 +56,7 @@
"arch": "x86_64"
},
"deploy": {
"type": "docker",
"type": "",
"docker": {
"image": "nginx",
"tag": "1.24.0",

repo.diff.view_file

@@ -139,9 +139,6 @@ func BuildK8sGetOptions(app *App, namespaceOverride string, wait bool) (*applica
if app == nil {
return nil, fmt.Errorf("nil app")
}
if strings.ToLower(strings.TrimSpace(app.Deploy.Type)) != "kubernetes" {
return nil, fmt.Errorf("deploy.type must be 'kubernetes'")
}
if app.Deploy.Kubernetes == nil {
return nil, fmt.Errorf("deploy.kubernetes is required")
}
@@ -160,9 +157,6 @@ func BuildK8sDeleteOptions(app *App, namespaceOverride string) (*application.Del
if app == nil {
return nil, fmt.Errorf("nil app")
}
if strings.ToLower(strings.TrimSpace(app.Deploy.Type)) != "kubernetes" {
return nil, fmt.Errorf("deploy.type must be 'kubernetes'")
}
if app.Deploy.Kubernetes == nil {
return nil, fmt.Errorf("deploy.kubernetes is required")
}
@@ -192,9 +186,6 @@ func BuildK8sUpdateOptions(app *App, namespaceOverride string, existing *applica
if app == nil {
return nil, fmt.Errorf("nil app")
}
if strings.ToLower(strings.TrimSpace(app.Deploy.Type)) != "kubernetes" {
return nil, fmt.Errorf("deploy.type must be 'kubernetes'")
}
if app.Deploy.Kubernetes == nil {
return nil, fmt.Errorf("deploy.kubernetes is required")
}

repo.diff.view_file

@@ -5,6 +5,7 @@ package appstore
import (
"context"
"fmt"
k8s "code.gitea.io/gitea/modules/k8s"
applicationv1 "code.gitea.io/gitea/modules/k8s/api/application/v1"
@@ -27,7 +28,14 @@ func NewK8sManager(ctx context.Context) *K8sManager {
// InstallAppToKubernetes installs an application to a Kubernetes cluster
func (km *K8sManager) InstallAppToKubernetes(app *App, kubeconfig []byte, contextName string) error {
// Validate that the app supports Kubernetes deployment
if app.Deploy.Type != "kubernetes" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
// 优先检查实际部署类型,如果为空则检查支持的部署类型
if app.Deploy.Type != "" && app.Deploy.Type != "kubernetes" {
return &AppStoreError{
Code: "DEPLOYMENT_TYPE_ERROR",
Message: "Application is not configured for Kubernetes deployment",
}
}
if app.Deploy.Type == "" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
return &AppStoreError{
Code: "DEPLOYMENT_TYPE_ERROR",
Message: "Application does not support Kubernetes deployment",
@@ -70,7 +78,14 @@ func (km *K8sManager) InstallAppToKubernetes(app *App, kubeconfig []byte, contex
// UninstallAppFromKubernetes uninstalls an application from a Kubernetes cluster
func (km *K8sManager) UninstallAppFromKubernetes(app *App, kubeconfig []byte, contextName string) error {
// Validate that the app supports Kubernetes deployment
if app.Deploy.Type != "kubernetes" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
// 优先检查实际部署类型,如果为空则检查支持的部署类型
if app.Deploy.Type != "" && app.Deploy.Type != "kubernetes" {
return &AppStoreError{
Code: "DEPLOYMENT_TYPE_ERROR",
Message: "Application is not configured for Kubernetes deployment",
}
}
if app.Deploy.Type == "" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
return &AppStoreError{
Code: "DEPLOYMENT_TYPE_ERROR",
Message: "Application does not support Kubernetes deployment",
@@ -113,7 +128,14 @@ func (km *K8sManager) UninstallAppFromKubernetes(app *App, kubeconfig []byte, co
// GetAppFromKubernetes gets an application from a Kubernetes cluster
func (km *K8sManager) GetAppFromKubernetes(app *App, kubeconfig []byte, contextName string) (interface{}, error) {
// Validate that the app supports Kubernetes deployment
if app.Deploy.Type != "kubernetes" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
// 优先检查实际部署类型,如果为空则检查支持的部署类型
if app.Deploy.Type != "" && app.Deploy.Type != "kubernetes" {
return nil, &AppStoreError{
Code: "DEPLOYMENT_TYPE_ERROR",
Message: "Application is not configured for Kubernetes deployment",
}
}
if app.Deploy.Type == "" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
return nil, &AppStoreError{
Code: "DEPLOYMENT_TYPE_ERROR",
Message: "Application does not support Kubernetes deployment",
@@ -147,7 +169,14 @@ func (km *K8sManager) GetAppFromKubernetes(app *App, kubeconfig []byte, contextN
// ListAppsFromKubernetes lists applications from a Kubernetes cluster
func (km *K8sManager) ListAppsFromKubernetes(app *App, kubeconfig []byte, contextName string) (*applicationv1.ApplicationList, error) {
// Validate that the app supports Kubernetes deployment
if app.Deploy.Type != "kubernetes" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
// 优先检查实际部署类型,如果为空则检查支持的部署类型
if app.Deploy.Type != "" && app.Deploy.Type != "kubernetes" {
return nil, &AppStoreError{
Code: "DEPLOYMENT_TYPE_ERROR",
Message: "Application is not configured for Kubernetes deployment",
}
}
if app.Deploy.Type == "" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
return nil, &AppStoreError{
Code: "DEPLOYMENT_TYPE_ERROR",
Message: "Application does not support Kubernetes deployment",
@@ -181,7 +210,14 @@ func (km *K8sManager) ListAppsFromKubernetes(app *App, kubeconfig []byte, contex
// UpdateAppInKubernetes updates an application in a Kubernetes cluster
func (km *K8sManager) UpdateAppInKubernetes(app *App, kubeconfig []byte, contextName string) error {
// Validate that the app supports Kubernetes deployment
if app.Deploy.Type != "kubernetes" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
// 优先检查实际部署类型,如果为空则检查支持的部署类型
if app.Deploy.Type != "" && app.Deploy.Type != "kubernetes" {
return &AppStoreError{
Code: "DEPLOYMENT_TYPE_ERROR",
Message: "Application is not configured for Kubernetes deployment",
}
}
if app.Deploy.Type == "" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
return &AppStoreError{
Code: "DEPLOYMENT_TYPE_ERROR",
Message: "Application does not support Kubernetes deployment",
@@ -198,8 +234,29 @@ func (km *K8sManager) UpdateAppInKubernetes(app *App, kubeconfig []byte, context
}
}
// Build Kubernetes update options
updateOptions, err := BuildK8sUpdateOptions(app, "", nil)
// 先获取现有的应用,以保留元数据(特别是 resourceVersion
existingApp, err := km.GetAppFromKubernetes(app, kubeconfig, contextName)
if err != nil {
// 如果应用不存在,返回错误
return &AppStoreError{
Code: "APP_NOT_FOUND",
Message: "应用在 Kubernetes 中不存在,无法更新",
Details: err.Error(),
}
}
// 确保获取到的是 Application 类型
appData, ok := existingApp.(*applicationv1.Application)
if !ok {
return &AppStoreError{
Code: "TYPE_ERROR",
Message: "无法获取现有应用数据",
Details: fmt.Sprintf("期望 *applicationv1.Application 类型,实际类型: %T", existingApp),
}
}
// Build Kubernetes update options传入现有应用以保留元数据
updateOptions, err := BuildK8sUpdateOptions(app, "", appData)
if err != nil {
return &AppStoreError{
Code: "KUBERNETES_OPTIONS_ERROR",
@@ -220,3 +277,8 @@ func (km *K8sManager) UpdateAppInKubernetes(app *App, kubeconfig []byte, context
return nil
}
// IsApplicationReady 封装底层就绪判断,避免上层直接依赖 k8s/application 的实现
func (km *K8sManager) IsApplicationReady(status *applicationv1.ApplicationStatus) bool {
return application.IsApplicationStatusReady(status)
}

repo.diff.view_file

@@ -6,6 +6,7 @@ package appstore
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
@@ -252,34 +253,34 @@ func (m *Manager) GetTags() ([]string, error) {
// PrepareInstallation prepares an application for installation by merging user config
// Returns a complete App structure with merged configuration
func (m *Manager) PrepareInstallation(appID string, userConfig UserConfig) (*App, error) {
// Load the application
app, err := m.GetApp(appID)
if err != nil {
return nil, err
}
// func (m *Manager) PrepareInstallation(appID string, userConfig UserConfig) (*App, error) {
// // Load the application
// app, err := m.GetApp(appID)
// if err != nil {
// return nil, err
// }
// Set the app ID and version in user config if not provided
if userConfig.AppID == "" {
userConfig.AppID = appID
}
if userConfig.Version == "" {
userConfig.Version = app.Version
}
// // Set the app ID and version in user config if not provided
// if userConfig.AppID == "" {
// userConfig.AppID = appID
// }
// if userConfig.Version == "" {
// userConfig.Version = app.Version
// }
// Merge user configuration with app's default configuration
mergedApp, err := m.parser.MergeUserConfig(app, userConfig)
if err != nil {
return nil, err
}
// // Merge user configuration with app's default configuration
// mergedApp, err := m.parser.MergeUserConfig(app, userConfig)
// if err != nil {
// return nil, err
// }
return mergedApp, nil
}
// return mergedApp, nil
// }
// AddApp adds a new application to the database
func (m *Manager) AddApp(app *App) error {
// Validate the app
if err := m.parser.validateApp(app); err != nil {
if err := m.parser.validateAppTemplate(app); err != nil {
return err
}
@@ -303,7 +304,7 @@ func (m *Manager) AddApp(app *App) error {
// AddAppFromJSON 通过 parser 解析原始 JSON 并添加应用
func (m *Manager) AddAppFromJSON(jsonBytes []byte) error {
app, err := m.parser.ParseApp(jsonBytes)
app, err := m.parser.ParseAppTemplate(jsonBytes)
if err != nil {
return err
}
@@ -469,6 +470,67 @@ func (m *Manager) InstallApp(appID string, configJSON string, installTarget stri
return nil
}
// UpdateInstalledApp updates an already installed application with new configuration
// The update flow mirrors InstallApp: merge config → choose target → call K8s/Docker updater
func (m *Manager) UpdateInstalledApp(appID string, configJSON string, installTarget string, kubeconfig string, kubeconfigContext string) error {
// 获取应用信息
app, err := m.GetApp(appID)
if err != nil {
return &AppStoreError{
Code: "APP_NOT_FOUND",
Message: "获取应用失败",
Details: err.Error(),
}
}
// 使用 parser 解析和合并用户配置
mergedApp, err := m.parser.ParseAndMergeUserConfig(app, configJSON)
if err != nil {
return &AppStoreError{
Code: "CONFIG_MERGE_ERROR",
Message: "配置合并失败",
Details: err.Error(),
}
}
// 根据安装目标和应用实际部署类型决定更新方式
if installTarget == "kubeconfig" && kubeconfig != "" {
// 更新外部 Kubernetes 集群中的应用实例
switch mergedApp.Deploy.Type {
case "kubernetes":
if err := m.UpdateAppInKubernetes(mergedApp, []byte(kubeconfig), kubeconfigContext); err != nil {
return &AppStoreError{Code: "KUBERNETES_UPDATE_ERROR", Message: "Kubernetes 更新失败", Details: err.Error()}
}
return nil
case "docker":
return &AppStoreError{Code: "NOT_IMPLEMENTED", Message: "外部环境 Docker 更新功能开发中"}
default:
return &AppStoreError{Code: "NOT_IMPLEMENTED", Message: "外部环境更新功能开发中"}
}
}
// 本地更新(依据应用部署类型)
switch mergedApp.Deploy.Type {
case "kubernetes":
if err := m.UpdateAppInKubernetes(mergedApp, nil, ""); err != nil {
return &AppStoreError{Code: "KUBERNETES_UPDATE_ERROR", Message: "本地 Kubernetes 更新失败", Details: err.Error()}
}
return nil
case "docker":
// TODO: 实现 Docker 更新逻辑
return &AppStoreError{Code: "NOT_IMPLEMENTED", Message: "本地 Docker 更新功能开发中"}
default:
// 未知部署类型,优先尝试 K8s若 deployment_type 支持)
if mergedApp.DeploymentType == "kubernetes" || mergedApp.DeploymentType == "both" {
if err := m.UpdateAppInKubernetes(mergedApp, nil, ""); err != nil {
return &AppStoreError{Code: "KUBERNETES_UPDATE_ERROR", Message: "本地 Kubernetes 更新失败", Details: err.Error()}
}
return nil
}
return &AppStoreError{Code: "NOT_IMPLEMENTED", Message: "本地更新功能开发中"}
}
}
// UninstallApp uninstalls an application based on the provided parameters
func (m *Manager) UninstallApp(appID string, installTarget string, kubeconfig string, kubeconfigContext string) error {
// 获取应用信息
@@ -492,10 +554,9 @@ func (m *Manager) UninstallApp(appID string, installTarget string, kubeconfig st
}
}
} else {
// 根据应用实际部署类型决定本地卸载方式
switch app.Deploy.Type {
case "kubernetes":
// 应用部署在 Kubernetes从本地 K8s 集群卸载
// 优先检查应用实际部署类型
if app.Deploy.Type == "kubernetes" {
// 应用实际部署在 Kubernetes从本地 K8s 集群卸载
if err := m.UninstallAppFromKubernetes(app, nil, ""); err != nil {
return &AppStoreError{
Code: "KUBERNETES_UNINSTALL_ERROR",
@@ -503,21 +564,40 @@ func (m *Manager) UninstallApp(appID string, installTarget string, kubeconfig st
Details: err.Error(),
}
}
case "docker":
// 应用部署在 Docker从本地 Docker 卸载
return nil
} else if app.Deploy.Type == "docker" {
// 应用实际部署在 Docker从本地 Docker 卸载
// TODO: 实现 Docker 卸载逻辑
return &AppStoreError{
Code: "NOT_IMPLEMENTED",
Message: "本地 Docker 卸载功能开发中",
}
default:
// 未知部署类型,默认尝试 Docker
// TODO: 实现 Docker 卸载逻辑
return &AppStoreError{
Code: "NOT_IMPLEMENTED",
Message: "本地卸载功能开发中",
} else if app.Deploy.Type == "" {
// 如果 Deploy.Type 为空,根据应用支持的部署类型尝试卸载
if app.DeploymentType == "kubernetes" || app.DeploymentType == "both" {
// 尝试从 Kubernetes 卸载
if err := m.UninstallAppFromKubernetes(app, nil, ""); err != nil {
return &AppStoreError{
Code: "KUBERNETES_UNINSTALL_ERROR",
Message: "本地 Kubernetes 卸载失败",
Details: err.Error(),
}
}
return nil
} else if app.DeploymentType == "docker" {
// TODO: 实现 Docker 卸载逻辑
return &AppStoreError{
Code: "NOT_IMPLEMENTED",
Message: "本地 Docker 卸载功能开发中",
}
}
}
// 如果以上都不匹配,返回未知部署类型错误
return &AppStoreError{
Code: "UNKNOWN_DEPLOYMENT_TYPE",
Message: "未知的部署类型,无法确定卸载方式",
}
}
return nil
@@ -547,3 +627,109 @@ func (m *Manager) ListAppsFromKubernetes(app *App, kubeconfig []byte, contextNam
func (m *Manager) UpdateAppInKubernetes(app *App, kubeconfig []byte, contextName string) error {
return m.k8s.UpdateAppInKubernetes(app, kubeconfig, contextName)
}
// IsInstalledAppReady 提供给上层的就绪判断入口,避免直接依赖 k8s 层实现
func (m *Manager) IsInstalledAppReady(status *applicationv1.ApplicationStatus) bool {
return m.k8s.IsApplicationReady(status)
}
// GetAppStatus 获取应用的安装和运行状态
func (m *Manager) GetAppStatus(appID string) (map[string]interface{}, error) {
app, err := m.GetApp(appID)
if err != nil {
return nil, &AppStoreError{
Code: "APP_NOT_FOUND",
Message: "应用不存在",
Details: err.Error(),
}
}
// 检查应用是否已安装
// 优先检查实际部署类型,如果为空则检查支持的部署类型
if app.Deploy.Type == "kubernetes" || (app.Deploy.Type == "" && (app.DeploymentType == "kubernetes" || app.DeploymentType == "both")) {
// 尝试从 Kubernetes 获取状态
k8sApp, err := m.GetAppFromKubernetes(app, nil, "")
if err != nil {
// 应用未安装或获取状态失败
return map[string]interface{}{
"phase": "Not Installed",
"replicas": 0,
"readyReplicas": 0,
"ready": false,
}, nil
}
// 应用已安装,返回状态信息
if k8sApp != nil {
// 尝试解析 K8s Application 状态
if appData, ok := k8sApp.(*applicationv1.Application); ok {
// 使用 Application CRD 的实际状态
phase := string(appData.Status.Phase)
replicas := int(appData.Status.Replicas)
readyReplicas := int(appData.Status.ReadyReplicas)
// 判断是否就绪
ready := m.IsInstalledAppReady(&appData.Status)
return map[string]interface{}{
"phase": phase,
"replicas": replicas,
"readyReplicas": readyReplicas,
"ready": ready,
}, nil
}
// 如果无法解析,返回错误
return nil, &AppStoreError{
Code: "STATUS_PARSE_ERROR",
Message: "无法解析应用状态数据",
Details: fmt.Sprintf("期望 *applicationv1.Application 类型,实际类型: %T", k8sApp),
}
}
}
// 默认返回未安装状态
return map[string]interface{}{
"phase": "Not Installed",
"replicas": 0,
"readyReplicas": 0,
"ready": false,
}, nil
}
// StopApp stops an application by setting replicas to 0
func (m *Manager) StopApp(appID string, installTarget string, kubeconfig []byte, kubeconfigContext string) error {
// 获取应用信息
app, err := m.GetApp(appID)
if err != nil {
return &AppStoreError{
Code: "APP_NOT_FOUND",
Message: "获取应用失败",
Details: err.Error(),
}
}
// 检查应用是否支持 Kubernetes 部署
if app.Deploy.Type == "kubernetes" || (app.Deploy.Type == "" && (app.DeploymentType == "kubernetes" || app.DeploymentType == "both")) {
// 创建应用的副本并设置副本数为 0
stoppedApp := *app
if stoppedApp.Deploy.Kubernetes != nil {
stoppedApp.Deploy.Kubernetes.Replicas = 0
}
// 调用 k8s 层的 UpdateAppInKubernetes 函数来停止应用
if err := m.k8s.UpdateAppInKubernetes(&stoppedApp, kubeconfig, kubeconfigContext); err != nil {
return &AppStoreError{
Code: "KUBERNETES_STOP_ERROR",
Message: "停止应用失败",
Details: err.Error(),
}
}
return nil
}
// 如果应用不支持 Kubernetes 部署,返回错误
return &AppStoreError{
Code: "UNSUPPORTED_DEPLOYMENT_TYPE",
Message: "应用不支持 Kubernetes 部署,无法停止",
}
}

repo.diff.view_file

@@ -77,8 +77,24 @@ func (p *Parser) MergeUserConfig(app *App, userConfig UserConfig) (*App, error)
return &mergedApp, nil
}
// ParseApp 从原始 JSON 字节流解析 App
func (p *Parser) ParseApp(jsonBytes []byte) (*App, error) {
// // ParseApp 从原始 JSON 字节流解析 App
// func (p *Parser) ParseApp(jsonBytes []byte) (*App, error) {
// var app App
// if err := json.Unmarshal(jsonBytes, &app); err != nil {
// return nil, &AppStoreError{
// Code: "INVALID_JSON",
// Message: "应用JSON格式错误",
// Details: err.Error(),
// }
// }
// if err := p.validateApp(&app); err != nil {
// return nil, err
// }
// return &app, nil
// }
// ParseAppTemplate parses JSON bytes into an App struct for app templates (deploy.type is optional)
func (p *Parser) ParseAppTemplate(jsonBytes []byte) (*App, error) {
var app App
if err := json.Unmarshal(jsonBytes, &app); err != nil {
return nil, &AppStoreError{
@@ -87,7 +103,7 @@ func (p *Parser) ParseApp(jsonBytes []byte) (*App, error) {
Details: err.Error(),
}
}
if err := p.validateApp(&app); err != nil {
if err := p.validateAppTemplate(&app); err != nil {
return nil, err
}
return &app, nil
@@ -98,7 +114,7 @@ func (p *Parser) ParseAndMergeApp(jsonBytes []byte, baseApp *App) (*App, error)
if baseApp == nil {
return nil, &AppStoreError{Code: "INTERNAL", Message: "baseApp is nil"}
}
parsed, err := p.ParseApp(jsonBytes)
parsed, err := p.ParseAppTemplate(jsonBytes)
if err != nil {
return nil, err
}
@@ -284,6 +300,68 @@ func (p *Parser) validateApp(app *App) error {
return nil
}
// validateAppTemplate validates application template (deploy.type is optional)
func (p *Parser) validateAppTemplate(app *App) error {
if app.ID == "" {
return &AppStoreError{
Code: "VALIDATION_ERROR",
Message: "App ID is required",
}
}
if app.Name == "" {
return &AppStoreError{
Code: "VALIDATION_ERROR",
Message: fmt.Sprintf("App name is required for app: %s", app.ID),
}
}
if app.Category == "" {
return &AppStoreError{
Code: "VALIDATION_ERROR",
Message: fmt.Sprintf("App category is required for app: %s", app.ID),
}
}
if app.Version == "" {
return &AppStoreError{
Code: "VALIDATION_ERROR",
Message: fmt.Sprintf("App version is required for app: %s", app.ID),
}
}
if err := p.validateVersionFormat(app.Version); err != nil {
return err
}
if err := p.validateAppConfig(&app.Config); err != nil {
return err
}
// For app templates, deploy.type is optional, but if provided, it must be valid
if app.Deploy.Type != "" {
if err := p.validateAppDeploy(&app.Deploy); err != nil {
return err
}
}
// Validate deployment_type value when provided
deploymentType := strings.ToLower(strings.TrimSpace(app.DeploymentType))
if deploymentType != "" {
switch deploymentType {
case "docker", "kubernetes", "both":
// allowed
default:
return &AppStoreError{
Code: "VALIDATION_ERROR",
Message: fmt.Sprintf("Invalid deployment_type: %s (must be docker|kubernetes|both)", app.DeploymentType),
}
}
}
return nil
}
// validateVersionFormat validates version string format
func (p *Parser) validateVersionFormat(version string) error {
// Simple version format validation (semantic versioning)

repo.diff.view_file

@@ -200,18 +200,14 @@
<!-- Deployment Type Filter -->
<div class="ui vertical fluid menu deployment-filter" style="margin-top: 1rem;">
<div class="header item">{{ctx.Locale.Tr "appstore.deployment_type"}}</div>
<a class="item active" data-deployment="all">
<i class="grid layout icon"></i>
{{ctx.Locale.Tr "appstore.category_all"}}
<a class="item active" data-deployment="kubernetes">
<i class="kubernetes icon"></i>
{{ctx.Locale.Tr "appstore.deployment_kubernetes"}}
</a>
<a class="item" data-deployment="docker">
<i class="docker icon"></i>
{{ctx.Locale.Tr "appstore.deployment_docker"}}
</a>
<a class="item" data-deployment="kubernetes">
<i class="kubernetes icon"></i>
{{ctx.Locale.Tr "appstore.deployment_kubernetes"}}
</a>
</div>
</div>
@@ -292,6 +288,11 @@
<div class="ui stackable grid">
<div class="eight wide column">
<div class="ui segment">
<h4 class="ui header">运行状态</h4>
<div id="details-status">
<span class="ui grey text">正在获取状态...</span>
</div>
<div class="ui divider"></div>
<h4 class="ui header">{{ctx.Locale.Tr "appstore.description"}}</h4>
<p id="details-description"></p>
@@ -329,7 +330,9 @@
</div>
<div class="actions">
<div class="ui button" onclick="closeDetailsModal()">{{ctx.Locale.Tr "cancel"}}</div>
<div class="ui primary button" onclick="showInstallModalFromDetails()">{{ctx.Locale.Tr "appstore.install"}}</div>
<div class="ui primary button" onclick="updateInstalledAppFromDetails()">更新应用</div>
<div class="ui orange button" onclick="stopCurrentAppFromDetails()">暂停应用</div>
<div class="ui red button" onclick="uninstallCurrentAppFromDetails()">卸载应用</div>
</div>
</div>
@@ -352,6 +355,7 @@
let allApps = [];
let filteredApps = [];
let currentApp = null;
let currentAppStatus = null; // 存储当前应用的运行状态
let storeSource = 'local'; // local | devstar
// 安装位置local | kubeconfig
let installTarget = 'local';
@@ -503,11 +507,9 @@ function loadApps() {
if (filteredApps.length === 0) {
grid.innerHTML = `
<div class="sixteen wide column">
<div class="ui placeholder segment">
<div class="ui icon header">
<i class="search icon"></i>
{{ctx.Locale.Tr "appstore.no_apps"}}
</div>
<div class="ui icon header">
<i class="search icon"></i>
{{ctx.Locale.Tr "appstore.no_apps"}}
</div>
</div>
`;
@@ -517,6 +519,8 @@ function loadApps() {
filteredApps.forEach(app => {
const card = createAppCard(app);
grid.appendChild(card);
// 异步检查应用安装状态
checkAppInstallStatus(app);
});
}
@@ -558,6 +562,12 @@ function createAppCard(app) {
deploymentLabel = '<span class="ui mini green label">Docker</span>';
}
// 初始显示安装按钮,后续异步检查状态
const actionButton = `<button class="ui primary compact button" onclick="showInstallModal('${app.id || app.app_id}')" id="install-btn-${app.id || app.app_id}">
<i class="download icon"></i>
安装
</button>`;
column.innerHTML = `
<div class="ui fluid card">
<div class="image">
@@ -588,10 +598,7 @@ function createAppCard(app) {
<i class="info circle icon"></i>
详情
</button>
<button class="ui primary compact button" onclick="showInstallModal('${app.id || app.app_id}')">
<i class="download icon"></i>
安装
</button>
${actionButton}
</div>
</div>
</div>
@@ -630,9 +637,7 @@ function filterApps() {
// Handle deployment type matching, including 'both' type
let deploymentMatch = false;
if (selectedDeployment === 'all') {
deploymentMatch = true;
} else if (appDeployment === 'both') {
if (appDeployment === 'both') {
// 'both' type matches both 'docker' and 'kubernetes' filters
deploymentMatch = true;
} else {
@@ -705,6 +710,12 @@ function showAppDetails(appId) {
if (modal) {
modal.style.display = 'block';
}
// 初始化按钮显示状态(在状态加载完成前先隐藏所有操作按钮)
updateActionButtonsVisibility();
// 拉取并展示运行状态
loadAndRenderAppStatus(app);
}
function showInstallModal(appId) {
@@ -820,7 +831,376 @@ function showInstallModalFromDetails() {
}
}
function installApp() {
// 更新应用(从详情弹窗触发)
async function updateInstalledAppFromDetails() {
if (!currentApp) return;
// 关闭详情弹窗,打开更新弹窗(类似安装)
closeDetailsModal();
showUpdateModal(currentApp.id || currentApp.app_id);
}
// 显示更新弹窗
function showUpdateModal(appId) {
const app = allApps.find(a => a.id === appId || a.app_id === appId);
if (!app) return;
currentApp = app;
document.getElementById('install-app-name').textContent = app.name + ' - 更新';
// 生成配置表单(重用安装的逻辑)
const form = document.getElementById('install-form');
const dyn = document.getElementById('install-dynamic-fields');
dyn.innerHTML = '';
// 配置表单生成逻辑(与 showInstallModal 相同)
let configSchema = null;
let configDefaults = null;
if (app.config && app.config.schema) {
configSchema = app.config.schema;
configDefaults = app.config.default || {};
} else if (app.Config && app.Config.Schema) {
configSchema = app.Config.Schema;
configDefaults = app.Config.Default || {};
}
if (!configSchema || typeof configSchema !== 'object') {
dyn.innerHTML = `
<div class="field">
<label>port <span style="color: red;">*</span></label>
<input type="number" name="port" value="80" required>
</div>
`;
} else {
Object.entries(configSchema).forEach(([key, config]) => {
if (!config) return;
let field = document.createElement('div');
field.className = 'field';
let inputHtml = '';
const defaultValue = configDefaults[key] || config.default || '';
if (config.type === 'int') {
const min = config.min || '';
const max = config.max || '';
inputHtml = `<input type="number" name="${key}" value="${defaultValue}" ${config.required ? 'required' : ''} ${min ? 'min="' + min + '"' : ''} ${max ? 'max="' + max + '"' : ''}>`;
} else if (config.type === 'string') {
inputHtml = `<input type="text" name="${key}" value="${defaultValue}" ${config.required ? 'required' : ''}">`;
} else if (config.type === 'bool' || config.type === 'boolean') {
inputHtml = `
<div class="ui checkbox">
<input type="checkbox" name="${key}" ${defaultValue ? 'checked' : ''}>
<label></label>
</div>
`;
} else if (config.type === 'select' && config.options) {
let optionsHtml = '';
config.options.forEach(option => {
const selected = option === defaultValue ? 'selected' : '';
optionsHtml += `<option value="${option}" ${selected}>${option}</option>`;
});
inputHtml = `<select name="${key}" class="ui dropdown" ${config.required ? 'required' : ''}>${optionsHtml}</select>`;
} else {
inputHtml = `<input type="text" name="${key}" value="${defaultValue}" ${config.required ? 'required' : ''}">`;
}
field.innerHTML = `
<label>${key} ${config.required ? '<span style="color: red;">*</span>' : ''}</label>
${config.description ? `<div class="ui small text">${config.description}</div>` : ''}
${inputHtml}
`;
dyn.appendChild(field);
});
}
// 设置安装位置表单
const radios = document.querySelectorAll('#install-modal input[name="installTargetRadio"]');
radios.forEach(r => { r.checked = (r.value === installTarget); });
const kubeconfigFields = document.querySelector('#install-modal #kubeconfig-fields');
if (kubeconfigFields) {
kubeconfigFields.style.display = (installTarget === 'kubeconfig') ? 'block' : 'none';
}
const kcContent = document.querySelector('#install-modal #kubeconfig-content');
const kcContext = document.querySelector('#install-modal #kubeconfig-context');
if (kcContent) kcContent.value = installKubeconfigContent || '';
if (kcContext) kcContext.value = installKubeconfigContext || '';
// 修改安装按钮文字为"更新"
const installBtn = document.querySelector('#install-modal .ui.primary.button[onclick="installApp()"]');
if (installBtn) {
installBtn.textContent = '更新应用';
installBtn.onclick = updateApp;
}
const modal = document.getElementById('install-modal');
if (modal) {
modal.style.display = 'block';
}
}
// 更新应用(表单提交)
function updateApp() {
if (!currentApp) return;
const form = document.getElementById('install-form');
const formData = new FormData(form);
const config = {};
for (let [key, value] of formData.entries()) {
const input = form.querySelector(`[name="${key}"]`);
if (input && input.type === 'checkbox') {
config[key] = input.checked;
} else if (input && input.type === 'number') {
config[key] = parseInt(value) || 0;
} else {
config[key] = value;
}
}
const appId = currentApp.id || currentApp.app_id;
const currentDeploymentFilter = document.querySelector('.deployment-filter .active');
const deploymentType = currentDeploymentFilter?.getAttribute('data-deployment');
// 从安装窗口读取安装位置
const selectedTarget = document.querySelector('#install-modal input[name="installTargetRadio"]:checked');
installTarget = selectedTarget ? selectedTarget.value : 'local';
if (installTarget === 'kubeconfig') {
const kcContentEl = document.querySelector('#install-modal #kubeconfig-content');
const kcContextEl = document.querySelector('#install-modal #kubeconfig-context');
installKubeconfigContent = kcContentEl ? kcContentEl.value.trim() : '';
installKubeconfigContext = kcContextEl ? kcContextEl.value.trim() : '';
if (!installKubeconfigContent) {
alert('请输入 kubeconfig 内容');
return;
}
config.deploy = { type: 'kubernetes' };
} else {
if (deploymentType === 'docker') {
config.deploy = { type: 'docker' };
} else if (deploymentType === 'kubernetes') {
config.deploy = { type: 'kubernetes' };
} else {
// 回退到 docker 作为默认选择
config.deploy = { type: 'docker' };
}
}
// 创建表单提交到更新路由
const updateForm = document.createElement('form');
updateForm.method = 'POST';
updateForm.action = `/user/settings/appstore/update/${appId}`;
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = '_csrf';
csrfInput.value = document.querySelector('meta[name="_csrf"]')?.content || '';
updateForm.appendChild(csrfInput);
const appIdInput = document.createElement('input');
appIdInput.type = 'hidden';
appIdInput.name = 'app_id';
appIdInput.value = appId;
updateForm.appendChild(appIdInput);
const configInput = document.createElement('input');
configInput.type = 'hidden';
configInput.name = 'config';
configInput.value = JSON.stringify(config);
updateForm.appendChild(configInput);
const targetInput = document.createElement('input');
targetInput.type = 'hidden';
targetInput.name = 'install_target';
targetInput.value = installTarget;
updateForm.appendChild(targetInput);
const redirectInput = document.createElement('input');
redirectInput.type = 'hidden';
redirectInput.name = 'redirect_to';
redirectInput.value = window.location.pathname;
updateForm.appendChild(redirectInput);
if (installTarget === 'kubeconfig') {
const kcInput = document.createElement('input');
kcInput.type = 'hidden';
kcInput.name = 'kubeconfig';
kcInput.value = installKubeconfigContent;
updateForm.appendChild(kcInput);
const kctxInput = document.createElement('input');
kctxInput.type = 'hidden';
kctxInput.name = 'kubeconfig_context';
kctxInput.value = installKubeconfigContext;
updateForm.appendChild(kctxInput);
}
document.body.appendChild(updateForm);
updateForm.submit();
closeInstallModal();
}
// 检查应用安装状态并更新按钮
async function checkAppInstallStatus(app) {
const appId = app.id || app.app_id;
const installBtn = document.getElementById(`install-btn-${appId}`);
if (!installBtn) return;
try {
const resp = await fetch(`/user/settings/appstore/api/status/${appId}`);
if (resp.ok) {
const data = await resp.json();
const phase = data.phase || data.status?.phase || 'Unknown';
const replicas = data.replicas ?? data.status?.replicas ?? 0;
const readyReplicas = data.readyReplicas ?? data.status?.readyReplicas ?? 0;
// 判断是否已安装且就绪
const isInstalled = phase !== 'Not Installed' && phase !== 'Unknown' && (replicas === 0 || readyReplicas > 0);
if (isInstalled) {
installBtn.className = 'ui green compact button disabled';
installBtn.innerHTML = '<i class="check icon"></i>已安装';
installBtn.onclick = null;
}
}
} catch (e) {
// 状态检查失败,保持安装按钮不变
console.log('Failed to check app status:', e);
}
}
// 卸载应用(从详情弹窗触发)
function uninstallCurrentAppFromDetails() {
if (!currentApp) return;
const appId = currentApp.id || currentApp.app_id;
const uninstallForm = document.createElement('form');
uninstallForm.method = 'POST';
uninstallForm.action = `/user/settings/appstore/uninstall/${appId}`;
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = '_csrf';
csrfInput.value = document.querySelector('meta[name="_csrf"]')?.content || '';
uninstallForm.appendChild(csrfInput);
const appIdInput = document.createElement('input');
appIdInput.type = 'hidden';
appIdInput.name = 'app_id';
appIdInput.value = appId;
uninstallForm.appendChild(appIdInput);
const redirectInput = document.createElement('input');
redirectInput.type = 'hidden';
redirectInput.name = 'redirect_to';
redirectInput.value = window.location.pathname;
uninstallForm.appendChild(redirectInput);
document.body.appendChild(uninstallForm);
uninstallForm.submit();
}
// 暂停应用(从详情弹窗触发)
function stopCurrentAppFromDetails() {
if (!currentApp) return;
const appId = currentApp.id || currentApp.app_id;
// 显示确认对话框
if (!confirm('确定要暂停应用吗?暂停后应用将停止运行,但数据会保留。')) {
return;
}
// 创建暂停应用的请求
const stopForm = document.createElement('form');
stopForm.method = 'POST';
stopForm.action = `/user/settings/appstore/stop/${appId}`;
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = '_csrf';
csrfInput.value = document.querySelector('meta[name="_csrf"]')?.content || '';
stopForm.appendChild(csrfInput);
const appIdInput = document.createElement('input');
appIdInput.type = 'hidden';
appIdInput.name = 'app_id';
appIdInput.value = appId;
stopForm.appendChild(appIdInput);
const redirectInput = document.createElement('input');
redirectInput.type = 'hidden';
redirectInput.name = 'redirect_to';
redirectInput.value = window.location.pathname;
stopForm.appendChild(redirectInput);
document.body.appendChild(stopForm);
stopForm.submit();
}
// 加载并渲染运行状态(基于 K8s Application CRD
async function loadAndRenderAppStatus(app) {
const statusEl = document.getElementById('details-status');
if (!statusEl || !app) return;
statusEl.innerHTML = '<span class="ui grey text">正在获取状态...</span>';
try {
const appId = app.id || app.app_id;
const resp = await fetch(`/user/settings/appstore/api/status/${appId}`);
if (!resp.ok) throw new Error(`status: ${resp.status}`);
const data = await resp.json();
// 期望后端返回 { phase, replicas, readyReplicas, ready } 或类似结构
const phase = data.phase || data.status?.phase || 'Unknown';
const replicas = data.replicas ?? data.status?.replicas ?? '-';
const readyReplicas = data.readyReplicas ?? data.status?.readyReplicas ?? '-';
const ready = (typeof data.ready === 'boolean')
? data.ready
: (phase !== 'Not Installed' && phase !== 'Unknown' && (replicas === 0 || readyReplicas > 0));
// 存储应用状态到全局变量
currentAppStatus = {
phase: phase,
replicas: replicas,
readyReplicas: readyReplicas,
ready: ready
};
statusEl.innerHTML = `
<div class="ui small statistic">
<div class="value">${ready ? '<span class="ui green text">Ready</span>' : '<span class="ui orange text">Not Ready</span>'}</div>
<div class="label">${phase} · ${readyReplicas}/${replicas}</div>
</div>
`;
// 根据状态动态控制按钮显示
updateActionButtonsVisibility();
} catch (e) {
statusEl.innerHTML = '<span class="ui red text">获取状态失败</span>';
currentAppStatus = null;
updateActionButtonsVisibility();
}
}
// 根据应用状态动态控制操作按钮的显示
function updateActionButtonsVisibility() {
const updateBtn = document.querySelector('#app-details-modal .ui.primary.button[onclick="updateInstalledAppFromDetails()"]');
const stopBtn = document.querySelector('#app-details-modal .ui.orange.button[onclick="stopCurrentAppFromDetails()"]');
const uninstallBtn = document.querySelector('#app-details-modal .ui.red.button[onclick="uninstallCurrentAppFromDetails()"]');
if (!currentAppStatus) {
// 状态未知,隐藏所有操作按钮
if (updateBtn) updateBtn.style.display = 'none';
if (stopBtn) stopBtn.style.display = 'none';
if (uninstallBtn) uninstallBtn.style.display = 'none';
return;
}
const isInstalled = currentAppStatus.phase !== 'Not Installed' && currentAppStatus.phase !== 'Unknown';
const isRunning = isInstalled && currentAppStatus.replicas > 0;
const isStopped = isInstalled && currentAppStatus.replicas === 0;
// 更新按钮:只有已安装的应用才能更新
if (updateBtn) {
updateBtn.style.display = isInstalled ? 'inline-block' : 'none';
}
// 暂停按钮:只有正在运行的应用才能暂停
if (stopBtn) {
stopBtn.style.display = isRunning ? 'inline-block' : 'none';
}
// 卸载按钮:只有已安装的应用才能卸载
if (uninstallBtn) {
uninstallBtn.style.display = isInstalled ? 'inline-block' : 'none';
}
}
async function installApp() {
if (!currentApp) return;
const form = document.getElementById('install-form');
const formData = new FormData(form);
@@ -857,11 +1237,8 @@ function installApp() {
} else if (deploymentType === 'kubernetes') {
config.deploy = { type: 'kubernetes' };
} else {
if (currentApp.deployment_type === 'both') {
config.deploy = { type: 'docker' };
} else {
config.deploy = { type: currentApp.deployment_type || 'docker' };
}
// 回退到 docker 作为默认选择
config.deploy = { type: 'docker' };
}
}
// 避免全局 beforeunload 提示:标记当前表单忽略脏检测
@@ -907,9 +1284,60 @@ function installApp() {
kctxInput.value = installKubeconfigContext;
installForm.appendChild(kctxInput);
}
// 提交安装表单
document.body.appendChild(installForm);
installForm.submit();
closeInstallModal();
// 显示安装中状态
const installBtn = document.querySelector('#install-modal .ui.primary.button');
if (installBtn) {
installBtn.className = 'ui loading primary button';
installBtn.textContent = '安装中...';
installBtn.disabled = true;
}
// 异步提交,成功后更新按钮状态
try {
const formData = new FormData(installForm);
const response = await fetch(installForm.action, {
method: 'POST',
body: formData
});
if (response.ok) {
// 安装成功,更新按钮状态
const appId = currentApp.id || currentApp.app_id;
const cardInstallBtn = document.getElementById(`install-btn-${appId}`);
if (cardInstallBtn) {
cardInstallBtn.className = 'ui green compact button disabled';
cardInstallBtn.innerHTML = '<i class="check icon"></i>已安装';
cardInstallBtn.onclick = null;
}
closeInstallModal();
// 显示成功消息
alert('应用安装成功!');
} else {
// 安装失败,恢复按钮状态
if (installBtn) {
installBtn.className = 'ui primary button';
installBtn.textContent = '安装应用';
installBtn.disabled = false;
}
const errorData = await response.json();
alert('安装失败: ' + (errorData.error || '未知错误'));
}
} catch (error) {
// 网络错误,恢复按钮状态
if (installBtn) {
installBtn.className = 'ui primary button';
installBtn.textContent = '安装应用';
installBtn.disabled = false;
}
alert('安装失败: ' + error.message);
}
// 清理表单
document.body.removeChild(installForm);
}
function showAddAppModal() {
@@ -928,27 +1356,48 @@ function closeAddAppModal() {
}
}
async function submitAddApp() {
console.log('submitAddApp called'); // 调试信息
const jsonText = document.getElementById('add-app-json').value.trim();
console.log('JSON text:', jsonText); // 调试信息
let appData;
try {
appData = JSON.parse(jsonText);
console.log('Parsed app data:', appData); // 调试信息
} catch (e) {
console.error('JSON parse error:', e); // 调试信息
alert('JSON格式错误');
return;
}
const resp = await fetch('/user/settings/appstore/api/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(appData)
});
if (resp.ok) {
closeAddAppModal();
loadAppsFromAPI(); // 刷新应用列表
alert('添加成功');
} else {
const err = await resp.json();
alert('添加失败: ' + (err.error || '未知错误'));
// 获取 CSRF token
const csrfToken = document.querySelector('meta[name="_csrf"]')?.content || '';
console.log('CSRF token:', csrfToken); // 调试信息
try {
const resp = await fetch('/user/settings/appstore/api/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(appData)
});
console.log('Response status:', resp.status); // 调试信息
if (resp.ok) {
closeAddAppModal();
loadAppsFromAPI(); // 刷新应用列表
alert('添加成功');
} else {
const err = await resp.json();
console.error('API error:', err); // 调试信息
alert('添加失败: ' + (err.error || '未知错误'));
}
} catch (error) {
console.error('Fetch error:', error); // 调试信息
alert('网络错误: ' + error.message);
}
}
</script>