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 @@ - +