diff --git a/agent/app/api/v2/app.go b/agent/app/api/v2/app.go
index 5e0cd1df8..55c8fbda7 100644
--- a/agent/app/api/v2/app.go
+++ b/agent/app/api/v2/app.go
@@ -220,3 +220,23 @@ func (b *BaseApi) GetAppIcon(c *gin.Context) {
c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
c.Data(http.StatusOK, "image/png", iconBytes)
}
+
+// @Tags App
+// @Summary Search app detail by appkey and version
+// @Accept json
+// @Param appId path integer true "app key"
+// @Param version path string true "app version"
+// @Success 200 {object} response.AppDetailSimpleDTO
+// @Security ApiKeyAuth
+// @Security Timestamp
+// @Router /apps/detail/node/:appKey/:version [get]
+func (b *BaseApi) GetAppDetailForNode(c *gin.Context) {
+ appKey := c.Param("appKey")
+ version := c.Param("version")
+ appDetailDTO, err := appService.GetAppDetailByKey(appKey, version)
+ if err != nil {
+ helper.InternalServer(c, err)
+ return
+ }
+ helper.SuccessWithData(c, appDetailDTO)
+}
diff --git a/agent/app/dto/app.go b/agent/app/dto/app.go
index 37c9c1958..e013976ed 100644
--- a/agent/app/dto/app.go
+++ b/agent/app/dto/app.go
@@ -84,25 +84,26 @@ type ExtraProperties struct {
}
type AppProperty struct {
- Name string `json:"name"`
- Type string `json:"type"`
- Tags []string `json:"tags"`
- ShortDescZh string `json:"shortDescZh" yaml:"shortDescZh"`
- ShortDescEn string `json:"shortDescEn" yaml:"shortDescEn"`
- Description Locale `json:"description"`
- Key string `json:"key"`
- Required []string `json:"Required"`
- CrossVersionUpdate bool `json:"crossVersionUpdate" yaml:"crossVersionUpdate"`
- Limit int `json:"limit" yaml:"limit"`
- Recommend int `json:"recommend" yaml:"recommend"`
- Website string `json:"website"`
- Github string `json:"github"`
- Document string `json:"document"`
- Architectures []string `json:"architectures"`
- MemoryRequired int `json:"memoryRequired" yaml:"memoryRequired"`
- GpuSupport bool `json:"gpuSupport" yaml:"gpuSupport"`
- Version float64 `json:"version"`
- Deprecated float64 `json:"deprecated"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Tags []string `json:"tags"`
+ ShortDescZh string `json:"shortDescZh" yaml:"shortDescZh"`
+ ShortDescEn string `json:"shortDescEn" yaml:"shortDescEn"`
+ Description Locale `json:"description"`
+ Key string `json:"key"`
+ Required []string `json:"Required"`
+ CrossVersionUpdate bool `json:"crossVersionUpdate" yaml:"crossVersionUpdate"`
+ Limit int `json:"limit" yaml:"limit"`
+ Recommend int `json:"recommend" yaml:"recommend"`
+ Website string `json:"website"`
+ Github string `json:"github"`
+ Document string `json:"document"`
+ Architectures []string `json:"architectures"`
+ MemoryRequired int `json:"memoryRequired" yaml:"memoryRequired"`
+ GpuSupport bool `json:"gpuSupport" yaml:"gpuSupport"`
+ Version float64 `json:"version"`
+ Deprecated float64 `json:"deprecated"`
+ BatchInstallSupport bool `json:"batchInstallSupport"`
}
type AppConfigVersion struct {
diff --git a/agent/app/dto/request/app.go b/agent/app/dto/request/app.go
index 3fd50b060..913e32e2f 100644
--- a/agent/app/dto/request/app.go
+++ b/agent/app/dto/request/app.go
@@ -22,7 +22,16 @@ type AppInstallCreate struct {
Name string `json:"name" validate:"required"`
Services map[string]string `json:"services"`
TaskID string `json:"taskID"`
+
AppContainerConfig
+ NodePushConfig
+}
+
+type NodePushConfig struct {
+ Nodes []string `json:"nodes"`
+ PushNode bool `json:"pushNode"`
+ AppKey string `json:"appKey"`
+ Version string `json:"version"`
}
type AppContainerConfig struct {
diff --git a/agent/app/dto/response/app.go b/agent/app/dto/response/app.go
index ab6498dc3..42fc3ba6c 100644
--- a/agent/app/dto/response/app.go
+++ b/agent/app/dto/response/app.go
@@ -29,17 +29,18 @@ type AppDTO struct {
}
type AppItem struct {
- Name string `json:"name"`
- Key string `json:"key"`
- ID uint `json:"id"`
- Description string `json:"description"`
- Status string `json:"status"`
- Installed bool `json:"installed"`
- Limit int `json:"limit"`
- Tags []string `json:"tags"`
- GpuSupport bool `json:"gpuSupport"`
- Recommend int `json:"recommend"`
- Type string `json:"type"`
+ Name string `json:"name"`
+ Key string `json:"key"`
+ ID uint `json:"id"`
+ Description string `json:"description"`
+ Status string `json:"status"`
+ Installed bool `json:"installed"`
+ Limit int `json:"limit"`
+ Tags []string `json:"tags"`
+ GpuSupport bool `json:"gpuSupport"`
+ Recommend int `json:"recommend"`
+ Type string `json:"type"`
+ BatchInstallSupport bool `json:"batchInstallSupport"`
}
type TagDTO struct {
@@ -75,6 +76,10 @@ type AppDetailDTO struct {
GpuSupport bool `json:"gpuSupport"`
}
+type AppDetailSimpleDTO struct {
+ ID uint `json:"id"`
+}
+
type IgnoredApp struct {
Icon string `json:"icon"`
Name string `json:"name"`
diff --git a/agent/app/model/app.go b/agent/app/model/app.go
index da8c8cb08..572bbc288 100644
--- a/agent/app/model/app.go
+++ b/agent/app/model/app.go
@@ -34,6 +34,7 @@ type App struct {
MemoryRequired int `json:"memoryRequired"`
GpuSupport bool `json:"gpuSupport"`
RequiredPanelVersion float64 `json:"requiredPanelVersion"`
+ BatchInstallSupport bool `json:"batchInstallSupport" yaml:"batchInstallSupport"`
Details []AppDetail `json:"-" gorm:"-:migration"`
TagsKey []string `json:"tags" yaml:"tags" gorm:"-"`
diff --git a/agent/app/service/app.go b/agent/app/service/app.go
index e4d4fbf1b..a84c270da 100644
--- a/agent/app/service/app.go
+++ b/agent/app/service/app.go
@@ -47,6 +47,7 @@ type IAppService interface {
GetAppDetailByID(id uint) (*response.AppDetailDTO, error)
SyncAppListFromLocal(taskID string)
GetAppIcon(appID uint) ([]byte, error)
+ GetAppDetailByKey(appKey, version string) (response.AppDetailSimpleDTO, error)
}
func NewIAppService() IAppService {
@@ -118,14 +119,15 @@ func (a AppService) PageApp(ctx *gin.Context, req request.AppSearch) (*response.
}
}
appDTO := &response.AppItem{
- ID: ap.ID,
- Name: ap.Name,
- Key: ap.Key,
- Limit: ap.Limit,
- GpuSupport: ap.GpuSupport,
- Recommend: ap.Recommend,
- Description: ap.GetDescription(ctx),
- Type: ap.Type,
+ ID: ap.ID,
+ Name: ap.Name,
+ Key: ap.Key,
+ Limit: ap.Limit,
+ GpuSupport: ap.GpuSupport,
+ Recommend: ap.Recommend,
+ Description: ap.GetDescription(ctx),
+ Type: ap.Type,
+ BatchInstallSupport: ap.BatchInstallSupport,
}
appDTOs = append(appDTOs, appDTO)
tags, err := getAppTags(ap.ID, lang)
@@ -203,6 +205,20 @@ func (a AppService) GetApp(ctx *gin.Context, key string) (*response.AppDTO, erro
return &appDTO, nil
}
+func (a AppService) GetAppDetailByKey(appKey, version string) (response.AppDetailSimpleDTO, error) {
+ var appDetailDTO response.AppDetailSimpleDTO
+ app, err := appRepo.GetFirst(appRepo.WithKey(appKey))
+ if err != nil {
+ return appDetailDTO, err
+ }
+ appDetail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(app.ID), appDetailRepo.WithVersion(version))
+ if err != nil {
+ return appDetailDTO, err
+ }
+ appDetailDTO.ID = appDetail.ID
+ return appDetailDTO, nil
+}
+
func (a AppService) GetAppDetail(appID uint, version, appType string) (response.AppDetailDTO, error) {
var (
appDetailDTO response.AppDetailDTO
@@ -386,11 +402,21 @@ func (a AppService) Install(req request.AppInstallCreate) (appInstall *model.App
App: app,
}
composeMap := make(map[string]interface{})
+ var composeRes []byte
if req.EditCompose {
if err = yaml.Unmarshal([]byte(req.DockerCompose), &composeMap); err != nil {
return
}
} else {
+ if appDetail.DockerCompose == "" {
+ dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.CONF.RemoteURL.AppRepo, global.CONF.Base.Mode, app.Key, appDetail.Version)
+ _, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
+ if err != nil {
+ return
+ }
+ appDetail.DockerCompose = string(composeRes)
+ _ = appDetailRepo.Update(context.Background(), appDetail)
+ }
if err = yaml.Unmarshal([]byte(appDetail.DockerCompose), &composeMap); err != nil {
return
}
diff --git a/agent/app/service/app_utils.go b/agent/app/service/app_utils.go
index 2c6e65096..1dda2de1d 100644
--- a/agent/app/service/app_utils.go
+++ b/agent/app/service/app_utils.go
@@ -1312,6 +1312,7 @@ func getApps(oldApps []model.App, items []dto.AppDefine, systemVersion string, t
app.MemoryRequired = config.MemoryRequired
app.Architectures = strings.Join(config.Architectures, ",")
app.GpuSupport = config.GpuSupport
+ app.BatchInstallSupport = config.BatchInstallSupport
apps[key] = app
}
return apps
diff --git a/agent/app/task/task.go b/agent/app/task/task.go
index 6abcea273..50ab4e97d 100644
--- a/agent/app/task/task.go
+++ b/agent/app/task/task.go
@@ -96,6 +96,7 @@ const (
TaskScopeCustomAppstore = "CustomAppstore"
TaskScopeTamper = "Tamper"
TaskScopeFileConvert = "Convert"
+ TaskScopeTask = "Task"
)
func GetTaskName(resourceName, operate, scope string) string {
diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go
index d0ed9cc88..80ff736ce 100644
--- a/agent/init/migration/migrate.go
+++ b/agent/init/migration/migrate.go
@@ -60,6 +60,7 @@ func InitAgentDB() {
migrations.UpdateWebsite,
migrations.AddisIPtoWebsiteSSL,
migrations.InitPingStatus,
+ migrations.UpdateApp,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go
index c954888e9..a105619cd 100644
--- a/agent/init/migration/migrations/init.go
+++ b/agent/init/migration/migrations/init.go
@@ -800,3 +800,10 @@ var InitPingStatus = &gormigrate.Migration{
return nil
},
}
+
+var UpdateApp = &gormigrate.Migration{
+ ID: "20251228-update-app",
+ Migrate: func(tx *gorm.DB) error {
+ return tx.AutoMigrate(&model.App{})
+ },
+}
diff --git a/agent/router/ro_app.go b/agent/router/ro_app.go
index 288040908..9f535fd14 100644
--- a/agent/router/ro_app.go
+++ b/agent/router/ro_app.go
@@ -19,6 +19,7 @@ func (a *AppRouter) InitRouter(Router *gin.RouterGroup) {
appRouter.POST("/search", baseApi.SearchApp)
appRouter.GET("/:key", baseApi.GetApp)
appRouter.GET("/detail/:appId/:version/:type", baseApi.GetAppDetail)
+ appRouter.GET("/detail/node/:appKey/:version", baseApi.GetAppDetailForNode)
appRouter.GET("/details/:id", baseApi.GetAppDetailByID)
appRouter.POST("/install", baseApi.InstallApp)
appRouter.GET("/tags", baseApi.GetAppTags)
diff --git a/core/app/task/task.go b/core/app/task/task.go
index 140d480f4..b82b82262 100644
--- a/core/app/task/task.go
+++ b/core/app/task/task.go
@@ -56,14 +56,16 @@ const (
TaskInstallCluster = "TaskInstallCluster"
TaskCreateCluster = "TaskCreateCluster"
TaskBackup = "TaskBackup"
+ TaskPush = "TaskPush"
)
const (
- TaskScopeSystem = "System"
- TaskScopeScript = "ScriptLibrary"
- TaskScopeNodeFile = "NodeFile"
- TaskScopeAppBackup = "AppBackup"
- TaskScopeCluster = "Cluster"
+ TaskScopeSystem = "System"
+ TaskScopeScript = "ScriptLibrary"
+ TaskScopeNodeFile = "NodeFile"
+ TaskScopeAppBackup = "AppBackup"
+ TaskScopeCluster = "Cluster"
+ TaskScopeAppInstall = "AppInstallTask"
)
func GetTaskName(resourceName, operate, scope string) string {
diff --git a/core/buserr/errors.go b/core/buserr/errors.go
index 5015ff84b..aab8f6a64 100644
--- a/core/buserr/errors.go
+++ b/core/buserr/errors.go
@@ -30,12 +30,16 @@ func (e BusinessError) Error() string {
return content
}
-func New(Key string) BusinessError {
- return BusinessError{
- Msg: Key,
- Detail: nil,
- Err: nil,
+func New(key string, opts ...Option) BusinessError {
+ be := BusinessError{
+ Msg: key,
}
+
+ for _, opt := range opts {
+ opt(&be)
+ }
+
+ return be
}
func WithErr(Key string, err error) BusinessError {
@@ -76,3 +80,28 @@ func WithName(Key string, name string) BusinessError {
Map: paramMap,
}
}
+
+type Option func(*BusinessError)
+
+func WithNameOption(name string) Option {
+ return func(be *BusinessError) {
+ if name != "" {
+ if be.Map == nil {
+ be.Map = make(map[string]interface{})
+ }
+ be.Map["name"] = name
+ }
+ }
+}
+
+func WithErrOption(err error) Option {
+ return func(be *BusinessError) {
+ be.Err = err
+ if err != nil {
+ if be.Map == nil {
+ be.Map = make(map[string]interface{})
+ }
+ be.Map["err"] = err
+ }
+ }
+}
diff --git a/core/i18n/lang/en.yaml b/core/i18n/lang/en.yaml
index 64d2e714a..82be6e9f9 100644
--- a/core/i18n/lang/en.yaml
+++ b/core/i18n/lang/en.yaml
@@ -110,6 +110,11 @@ FailedStatus: "{{ .name }} failed {{ .err }}"
Start: "Start"
SubTask: "Subtask"
Skip: "Skip errors and continue..."
+PushAppInstallTaskToNode: "Push app installation task to node [{{ .name }}]"
+TaskPush: "Push"
+AppInstallTask: "App installation task"
+PushAppFailed: "Failed to push app installation task"
+Success: "Success"
#script
ScriptLibrary: "Script Library"
diff --git a/core/i18n/lang/es-ES.yaml b/core/i18n/lang/es-ES.yaml
index 8c08b6850..f38034ce8 100644
--- a/core/i18n/lang/es-ES.yaml
+++ b/core/i18n/lang/es-ES.yaml
@@ -14,6 +14,11 @@ ErrApiConfigKeyInvalid: "Error de clave API: {{ .detail }}"
ErrApiConfigIPInvalid: "La IP de la solicitud API no está en la lista blanca: {{ .detail }}"
ErrApiConfigDisable: "Esta interfaz prohíbe llamadas a la API: {{ .detail }}"
ErrApiConfigKeyTimeInvalid: "Error de marca de tiempo de API: {{ .detail }}"
+PushAppInstallTaskToNode: "Enviar tarea de instalación de aplicación al nodo [{{ .name }}]"
+TaskPush: "Enviar"
+AppInstallTask: "Tarea de instalación de aplicación"
+PushAppFailed: "Error al enviar tarea de instalación de aplicación"
+Success: "Éxito"
# request
ErrNoSuchHost: "No se pudo encontrar el servidor solicitado {{ .err }}"
diff --git a/core/i18n/lang/ja.yaml b/core/i18n/lang/ja.yaml
index 9df2504d2..8a38064a8 100644
--- a/core/i18n/lang/ja.yaml
+++ b/core/i18n/lang/ja.yaml
@@ -111,6 +111,11 @@ FailedStatus: "{{ .name }} 失敗 {{ .err }}"
Start: "開始"
SubTask: "サブタスク"
Skip: "エラーを無視して続行..."
+PushAppInstallTaskToNode: "アプリインストールタスクをノード [{{ .name }}] にプッシュ"
+TaskPush: "プッシュ"
+AppInstallTask: "アプリインストールタスク"
+PushAppFailed: "アプリインストールタスクのプッシュに失敗しました"
+Success: "成功"
#script
ScriptLibrary: "スクリプトライブラリ"
diff --git a/core/i18n/lang/ko.yaml b/core/i18n/lang/ko.yaml
index edec07d71..4d5a4b6b3 100644
--- a/core/i18n/lang/ko.yaml
+++ b/core/i18n/lang/ko.yaml
@@ -110,6 +110,11 @@ FailedStatus: "{{ .name }} 실패 {{ .err }}"
Start: "시작"
SubTask: "서브 작업"
Skip: "오류 무시하고 계속..."
+PushAppInstallTaskToNode: "노드 [{{ .name }}]에 앱 설치 작업 푸시"
+TaskPush: "푸시"
+AppInstallTask: "앱 설치 작업"
+PushAppFailed: "앱 설치 작업 푸시 실패"
+Success: "성공"
#script
ScriptLibrary: "스크립트 라이브러리"
diff --git a/core/i18n/lang/ms.yaml b/core/i18n/lang/ms.yaml
index 408e63fcc..88671ea58 100644
--- a/core/i18n/lang/ms.yaml
+++ b/core/i18n/lang/ms.yaml
@@ -105,6 +105,11 @@ FailedStatus: "{{ .name }} gagal {{ .err }}"
Start: "Mula"
SubTask: "Tugas Sub"
Skip: "Abaikan ralat dan teruskan..."
+PushAppInstallTaskToNode: "Tolak tugas pemasangan aplikasi ke nod [{{ .name }}]"
+TaskPush: "Tolak"
+AppInstallTask: "Tugas pemasangan aplikasi"
+PushAppFailed: "Gagal menolak tugas pemasangan aplikasi"
+Success: "Berjaya"
#script
ScriptLibrary: "Pustaka Skrip"
diff --git a/core/i18n/lang/pt-BR.yaml b/core/i18n/lang/pt-BR.yaml
index 31c89ae66..099768064 100644
--- a/core/i18n/lang/pt-BR.yaml
+++ b/core/i18n/lang/pt-BR.yaml
@@ -110,6 +110,11 @@ FailedStatus: "{{ .name }} falhou {{ .err }}"
Start: "Iniciar"
SubTask: "Subtarefa"
Skip: "Ignorar erros e continuar..."
+PushAppInstallTaskToNode: "Enviar tarefa de instalação de aplicativo para o nó [{{ .name }}]"
+TaskPush: "Enviar"
+AppInstallTask: "Tarefa de instalação de aplicativo"
+PushAppFailed: "Falha ao enviar tarefa de instalação de aplicativo"
+Success: "Sucesso"
#script
ScriptLibrary: "Biblioteca de Scripts"
diff --git a/core/i18n/lang/ru.yaml b/core/i18n/lang/ru.yaml
index 769a24e7a..fa342f71c 100644
--- a/core/i18n/lang/ru.yaml
+++ b/core/i18n/lang/ru.yaml
@@ -110,6 +110,10 @@ FailedStatus: "{{ .name }} не удалось {{ .err }}"
Start: "Начать"
SubTask: "Подзадача"
Skip: "Пропустить ошибки и продолжить..."
+TaskPush: "Отправить"
+AppInstallTask: "Задача установки приложения"
+PushAppFailed: "Не удалось отправить задачу установки приложения"
+Success: "Успешно"
#script
ScriptLibrary: "Библиотека скриптов"
diff --git a/core/i18n/lang/tr.yaml b/core/i18n/lang/tr.yaml
index 8ade7be81..e71b8e1b9 100644
--- a/core/i18n/lang/tr.yaml
+++ b/core/i18n/lang/tr.yaml
@@ -109,6 +109,11 @@ FailedStatus: "{{ .name }} başarısız {{ .err }}"
Start: "Başla"
SubTask: "Alt görev"
Skip: "Hataları atla ve devam et..."
+PushAppInstallTaskToNode: "Uygulama kurulum görevini düğüme [{{ .name }}] gönder"
+TaskPush: "Gönder"
+AppInstallTask: "Uygulama kurulum görevi"
+PushAppFailed: "Uygulama kurulum görevi gönderilemedi"
+Success: "Başarılı"
#script
ScriptLibrary: "Betik Kütüphanesi"
diff --git a/core/i18n/lang/zh-Hant.yaml b/core/i18n/lang/zh-Hant.yaml
index bb673f2d4..964bed36c 100644
--- a/core/i18n/lang/zh-Hant.yaml
+++ b/core/i18n/lang/zh-Hant.yaml
@@ -110,6 +110,11 @@ FailedStatus: "{{ .name }} 失敗 {{ .err }}"
Start: "開始"
SubTask: "子任務"
Skip: "忽略錯誤並繼續..."
+PushAppInstallTaskToNode: "推送應用安裝任務到節點 [{{ .name }}]"
+TaskPush: "推送"
+AppInstallTask: "應用安裝任務"
+PushAppFailed: "推送應用安裝任務失敗"
+Success: "成功"
#script
ScriptLibrary: "腳本庫"
diff --git a/core/i18n/lang/zh.yaml b/core/i18n/lang/zh.yaml
index ef45b58a9..d80daf989 100644
--- a/core/i18n/lang/zh.yaml
+++ b/core/i18n/lang/zh.yaml
@@ -110,6 +110,11 @@ FailedStatus: "{{ .name }} 失败 {{ .err }}"
Start: "开始"
SubTask: "子任务"
Skip: "忽略错误并继续..."
+PushAppInstallTaskToNode: "推送应用安装任务到节点 [{{ .name }}]"
+TaskPush: "推送"
+AppInstallTask: "应用安装任务"
+PushAppFailed: "推送应用安装任务失败"
+Success: "成功"
#script
ScriptLibrary: "脚本库"
diff --git a/frontend/src/api/interface/app.ts b/frontend/src/api/interface/app.ts
index 77d436194..921feeeae 100644
--- a/frontend/src/api/interface/app.ts
+++ b/frontend/src/api/interface/app.ts
@@ -17,6 +17,7 @@ export namespace App {
website?: string;
github?: string;
readme: string;
+ batchInstallSupport: boolean;
}
interface Locale {
@@ -55,6 +56,7 @@ export namespace App {
tags: string[];
gpuSupport: boolean;
recommend: number;
+ batchInstallSupport: boolean;
}
export interface AppResPage {
@@ -132,6 +134,7 @@ export namespace App {
appDetailId: number;
params: any;
taskID: string;
+ name: string;
}
export interface AppInstallSearch extends ReqPage {
diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts
index 6880a62d3..b7c05b03d 100644
--- a/frontend/src/lang/modules/en.ts
+++ b/frontend/src/lang/modules/en.ts
@@ -3872,6 +3872,8 @@ const message = {
'Currently only supports migrating monolithic applications and applications associated only with MySQL, MariaDB, PostgreSQL databases',
opensslHelper:
'If using encrypted backup, the OpenSSL versions between the two nodes must be consistent, otherwise migration may fail.',
+ installApp: 'Batch install',
+ installAppHelper: 'Batch install apps to selected nodes',
},
alert: {
isAlert: 'Alert',
diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts
index 5b3744555..f9ac932be 100644
--- a/frontend/src/lang/modules/es-es.ts
+++ b/frontend/src/lang/modules/es-es.ts
@@ -3834,6 +3834,8 @@ const message = {
'Actualmente solo admite la migración de aplicaciones monolíticas y aplicaciones asociadas únicamente con bases de datos MySQL, MariaDB, PostgreSQL',
opensslHelper:
'Si se utiliza copia de seguridad cifrada, las versiones de OpenSSL entre los dos nodos deben ser consistentes, de lo contrario la migración puede fallar.',
+ installApp: 'Instalación por lotes',
+ installAppHelper: 'Instalar aplicaciones por lotes en los nodos seleccionados',
},
alert: {
isAlert: 'Alerta',
diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts
index a421ed575..61dca39ec 100644
--- a/frontend/src/lang/modules/ja.ts
+++ b/frontend/src/lang/modules/ja.ts
@@ -3762,6 +3762,8 @@ const message = {
'現在、単体アプリケーションと MySQL、MariaDB、PostgreSQL データベースのみに関連するアプリケーションの移行のみをサポートしています',
opensslHelper:
'暗号化バックアップを使用する場合、2つのノード間のOpenSSLバージョンは一致している必要があります。そうしないと、移行が失敗する可能性があります。',
+ installApp: '一括インストール',
+ installAppHelper: '選択したノードにアプリを一括インストール',
},
alert: {
isAlert: 'アラート',
diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts
index 97806271f..ab53e80c8 100644
--- a/frontend/src/lang/modules/ko.ts
+++ b/frontend/src/lang/modules/ko.ts
@@ -3695,6 +3695,8 @@ const message = {
'현재 단일 애플리케이션과 MySQL, MariaDB, PostgreSQL 데이터베이스만 연결된 애플리케이션의 마이그레이션만 지원합니다',
opensslHelper:
'암호화된 백업을 사용하는 경우 두 노드 간의 OpenSSL 버전이 일치해야 합니다. 그렇지 않으면 마이그레이션이 실패할 수 있습니다.',
+ installApp: '일괄 설치',
+ installAppHelper: '선택한 노드에 앱 일괄 설치',
},
alert: {
isAlert: '알림',
diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts
index 7b66368dc..59da3b343 100644
--- a/frontend/src/lang/modules/ms.ts
+++ b/frontend/src/lang/modules/ms.ts
@@ -3832,6 +3832,8 @@ const message = {
'Kini hanya menyokong penghijrahan aplikasi monolitik dan aplikasi yang hanya dikaitkan dengan pangkalan data MySQL, MariaDB, PostgreSQL',
opensslHelper:
'Jika menggunakan sandaran terenkripsi, versi OpenSSL antara dua nod mesti konsisten, jika tidak penghijrahan mungkin gagal.',
+ installApp: 'Pemasangan kelompok',
+ installAppHelper: 'Pasang aplikasi secara kelompok ke nod yang dipilih',
},
alert: {
isAlert: 'Amaran',
diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts
index 6a8548c10..fc23f103e 100644
--- a/frontend/src/lang/modules/pt-br.ts
+++ b/frontend/src/lang/modules/pt-br.ts
@@ -3851,6 +3851,8 @@ const message = {
'Atualmente suporta apenas a migração de aplicações monolíticas e aplicações associadas apenas a bancos de dados MySQL, MariaDB, PostgreSQL',
opensslHelper:
'Se usar backup criptografado, as versões do OpenSSL entre os dois nós devem ser consistentes, caso contrário a migração pode falhar.',
+ installApp: 'Instalação em lote',
+ installAppHelper: 'Instalar aplicativos em lote nos nós selecionados',
},
alert: {
isAlert: 'Alerta',
diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts
index d71fbd612..6bff10243 100644
--- a/frontend/src/lang/modules/ru.ts
+++ b/frontend/src/lang/modules/ru.ts
@@ -3838,6 +3838,8 @@ const message = {
'В настоящее время поддерживает миграцию только монолитных приложений и приложений, связанных только с базами данных MySQL, MariaDB, PostgreSQL',
opensslHelper:
'При использовании зашифрованного резервного копирования версии OpenSSL между двумя узлами должны быть согласованы, иначе миграция может завершиться неудачей.',
+ installApp: 'Пакетная установка',
+ installAppHelper: 'Пакетная установка приложений на выбранные узлы',
},
alert: {
isAlert: 'Оповещение',
diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts
index 8833d4a3b..7c7332d26 100644
--- a/frontend/src/lang/modules/tr.ts
+++ b/frontend/src/lang/modules/tr.ts
@@ -3913,6 +3913,8 @@ const message = {
'Şu anda yalnızca tek parça uygulamaların ve yalnızca MySQL, MariaDB, PostgreSQL veritabanlarıyla ilişkili uygulamaların taşınmasını destekler',
opensslHelper:
'Şifreli yedekleme kullanılıyorsa, iki düğüm arasındaki OpenSSL sürümleri tutarlı olmalıdır, aksi takdirde geçiş başarısız olabilir.',
+ installApp: 'Toplu kurulum',
+ installAppHelper: 'Seçilen düğümlere uygulamaları toplu kur',
},
alert: {
isAlert: 'Uyarı',
diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts
index 905fa1d3a..90fdcf25b 100644
--- a/frontend/src/lang/modules/zh-Hant.ts
+++ b/frontend/src/lang/modules/zh-Hant.ts
@@ -3573,6 +3573,8 @@ const message = {
nodeHelper: '不能選擇當前節點',
migrateHelper: '目前僅支持遷移單體應用和只關聯 MySQL、MariaDB、PostgreSQL 數據庫的應用',
opensslHelper: '如果使用加密備份,兩個節點之間的 OpenSSL 版本必須保持一致,否則可能導致遷移失敗。',
+ installApp: '批量安裝',
+ installAppHelper: '批量安裝應用到選擇的節點中',
},
alert: {
isAlert: '是否告警',
diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts
index 27c6d354f..88ffe0c71 100644
--- a/frontend/src/lang/modules/zh.ts
+++ b/frontend/src/lang/modules/zh.ts
@@ -3566,6 +3566,8 @@ const message = {
nodeHelper: '不能选择当前节点',
migrateHelper: '当前仅支持迁移单体应用和只关联 MySQL、MariaDB、PostgreSQL 数据库的应用',
opensslHelper: '如果使用加密备份,两个节点之间的 openssl 版本必须保持一致,不然会导致迁移失败',
+ installApp: '批量安装',
+ installAppHelper: '批量安装应用到选择的节点中',
},
alert: {
isAlert: '是否告警',
diff --git a/frontend/src/views/app-store/detail/form/index.vue b/frontend/src/views/app-store/detail/form/index.vue
index 64e187f6d..ca6fa0ebd 100644
--- a/frontend/src/views/app-store/detail/form/index.vue
+++ b/frontend/src/views/app-store/detail/form/index.vue
@@ -122,6 +122,15 @@
{{ $t('app.pullImageHelper') }}
+
+
{{ $t('app.editComposeHelper') }}
@@ -147,7 +156,16 @@ import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import { computeSizeFromMB } from '@/utils/util';
import { loadResourceLimit } from '@/api/modules/container';
import { useGlobalStore } from '@/composables/useGlobalStore';
-const { isOffLine } = useGlobalStore();
+const { isOffLine, isMasterProductPro, isMaster } = useGlobalStore();
+
+const PushtoNode = defineAsyncComponent(async () => {
+ const modules = import.meta.glob('@/xpack/views/ssl/index.vue');
+ const loader = modules['/src/xpack/views/ssl/index.vue'];
+ if (loader) {
+ return ((await loader()) as any).default;
+ }
+ return { template: '' };
+});
interface ClusterProps {
key: string;
@@ -161,6 +179,7 @@ interface ClusterProps {
interface Props {
loading?: boolean;
modelValue?: any;
+ batchInstallSupport?: boolean;
}
const limits = ref({
@@ -170,6 +189,7 @@ const limits = ref({
const props = withDefaults(defineProps(), {
loading: false,
+ batchInstallSupport: false,
});
interface Emits {
@@ -223,6 +243,8 @@ const initFormData = () => ({
gpuConfig: false,
specifyIP: '',
restartPolicy: 'always',
+ pushNode: false,
+ nodes: [],
});
const formData = ref(props.modelValue || initFormData());
diff --git a/frontend/src/views/app-store/detail/index.vue b/frontend/src/views/app-store/detail/index.vue
index 83d0265df..0a780642c 100644
--- a/frontend/src/views/app-store/detail/index.vue
+++ b/frontend/src/views/app-store/detail/index.vue
@@ -66,22 +66,22 @@
-
+