mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-01-12 10:04:39 +08:00
feat: Support batch installation of applications (#11539)
This commit is contained in:
parent
8fb3e78554
commit
e4ee07d36b
38 changed files with 269 additions and 55 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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:"-"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ const (
|
|||
TaskScopeCustomAppstore = "CustomAppstore"
|
||||
TaskScopeTamper = "Tamper"
|
||||
TaskScopeFileConvert = "Convert"
|
||||
TaskScopeTask = "Task"
|
||||
)
|
||||
|
||||
func GetTaskName(resourceName, operate, scope string) string {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ func InitAgentDB() {
|
|||
migrations.UpdateWebsite,
|
||||
migrations.AddisIPtoWebsiteSSL,
|
||||
migrations.InitPingStatus,
|
||||
migrations.UpdateApp,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
|
|
|||
|
|
@ -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{})
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 }}"
|
||||
|
|
|
|||
|
|
@ -111,6 +111,11 @@ FailedStatus: "{{ .name }} 失敗 {{ .err }}"
|
|||
Start: "開始"
|
||||
SubTask: "サブタスク"
|
||||
Skip: "エラーを無視して続行..."
|
||||
PushAppInstallTaskToNode: "アプリインストールタスクをノード [{{ .name }}] にプッシュ"
|
||||
TaskPush: "プッシュ"
|
||||
AppInstallTask: "アプリインストールタスク"
|
||||
PushAppFailed: "アプリインストールタスクのプッシュに失敗しました"
|
||||
Success: "成功"
|
||||
|
||||
#script
|
||||
ScriptLibrary: "スクリプトライブラリ"
|
||||
|
|
|
|||
|
|
@ -110,6 +110,11 @@ FailedStatus: "{{ .name }} 실패 {{ .err }}"
|
|||
Start: "시작"
|
||||
SubTask: "서브 작업"
|
||||
Skip: "오류 무시하고 계속..."
|
||||
PushAppInstallTaskToNode: "노드 [{{ .name }}]에 앱 설치 작업 푸시"
|
||||
TaskPush: "푸시"
|
||||
AppInstallTask: "앱 설치 작업"
|
||||
PushAppFailed: "앱 설치 작업 푸시 실패"
|
||||
Success: "성공"
|
||||
|
||||
#script
|
||||
ScriptLibrary: "스크립트 라이브러리"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -110,6 +110,10 @@ FailedStatus: "{{ .name }} не удалось {{ .err }}"
|
|||
Start: "Начать"
|
||||
SubTask: "Подзадача"
|
||||
Skip: "Пропустить ошибки и продолжить..."
|
||||
TaskPush: "Отправить"
|
||||
AppInstallTask: "Задача установки приложения"
|
||||
PushAppFailed: "Не удалось отправить задачу установки приложения"
|
||||
Success: "Успешно"
|
||||
|
||||
#script
|
||||
ScriptLibrary: "Библиотека скриптов"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -110,6 +110,11 @@ FailedStatus: "{{ .name }} 失敗 {{ .err }}"
|
|||
Start: "開始"
|
||||
SubTask: "子任務"
|
||||
Skip: "忽略錯誤並繼續..."
|
||||
PushAppInstallTaskToNode: "推送應用安裝任務到節點 [{{ .name }}]"
|
||||
TaskPush: "推送"
|
||||
AppInstallTask: "應用安裝任務"
|
||||
PushAppFailed: "推送應用安裝任務失敗"
|
||||
Success: "成功"
|
||||
|
||||
#script
|
||||
ScriptLibrary: "腳本庫"
|
||||
|
|
|
|||
|
|
@ -110,6 +110,11 @@ FailedStatus: "{{ .name }} 失败 {{ .err }}"
|
|||
Start: "开始"
|
||||
SubTask: "子任务"
|
||||
Skip: "忽略错误并继续..."
|
||||
PushAppInstallTaskToNode: "推送应用安装任务到节点 [{{ .name }}]"
|
||||
TaskPush: "推送"
|
||||
AppInstallTask: "应用安装任务"
|
||||
PushAppFailed: "推送应用安装任务失败"
|
||||
Success: "成功"
|
||||
|
||||
#script
|
||||
ScriptLibrary: "脚本库"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -3762,6 +3762,8 @@ const message = {
|
|||
'現在、単体アプリケーションと MySQL、MariaDB、PostgreSQL データベースのみに関連するアプリケーションの移行のみをサポートしています',
|
||||
opensslHelper:
|
||||
'暗号化バックアップを使用する場合、2つのノード間のOpenSSLバージョンは一致している必要があります。そうしないと、移行が失敗する可能性があります。',
|
||||
installApp: '一括インストール',
|
||||
installAppHelper: '選択したノードにアプリを一括インストール',
|
||||
},
|
||||
alert: {
|
||||
isAlert: 'アラート',
|
||||
|
|
|
|||
|
|
@ -3695,6 +3695,8 @@ const message = {
|
|||
'현재 단일 애플리케이션과 MySQL, MariaDB, PostgreSQL 데이터베이스만 연결된 애플리케이션의 마이그레이션만 지원합니다',
|
||||
opensslHelper:
|
||||
'암호화된 백업을 사용하는 경우 두 노드 간의 OpenSSL 버전이 일치해야 합니다. 그렇지 않으면 마이그레이션이 실패할 수 있습니다.',
|
||||
installApp: '일괄 설치',
|
||||
installAppHelper: '선택한 노드에 앱 일괄 설치',
|
||||
},
|
||||
alert: {
|
||||
isAlert: '알림',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -3838,6 +3838,8 @@ const message = {
|
|||
'В настоящее время поддерживает миграцию только монолитных приложений и приложений, связанных только с базами данных MySQL, MariaDB, PostgreSQL',
|
||||
opensslHelper:
|
||||
'При использовании зашифрованного резервного копирования версии OpenSSL между двумя узлами должны быть согласованы, иначе миграция может завершиться неудачей.',
|
||||
installApp: 'Пакетная установка',
|
||||
installAppHelper: 'Пакетная установка приложений на выбранные узлы',
|
||||
},
|
||||
alert: {
|
||||
isAlert: 'Оповещение',
|
||||
|
|
|
|||
|
|
@ -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ı',
|
||||
|
|
|
|||
|
|
@ -3573,6 +3573,8 @@ const message = {
|
|||
nodeHelper: '不能選擇當前節點',
|
||||
migrateHelper: '目前僅支持遷移單體應用和只關聯 MySQL、MariaDB、PostgreSQL 數據庫的應用',
|
||||
opensslHelper: '如果使用加密備份,兩個節點之間的 OpenSSL 版本必須保持一致,否則可能導致遷移失敗。',
|
||||
installApp: '批量安裝',
|
||||
installAppHelper: '批量安裝應用到選擇的節點中',
|
||||
},
|
||||
alert: {
|
||||
isAlert: '是否告警',
|
||||
|
|
|
|||
|
|
@ -3566,6 +3566,8 @@ const message = {
|
|||
nodeHelper: '不能选择当前节点',
|
||||
migrateHelper: '当前仅支持迁移单体应用和只关联 MySQL、MariaDB、PostgreSQL 数据库的应用',
|
||||
opensslHelper: '如果使用加密备份,两个节点之间的 openssl 版本必须保持一致,不然会导致迁移失败',
|
||||
installApp: '批量安装',
|
||||
installAppHelper: '批量安装应用到选择的节点中',
|
||||
},
|
||||
alert: {
|
||||
isAlert: '是否告警',
|
||||
|
|
|
|||
|
|
@ -122,6 +122,15 @@
|
|||
<span class="input-help">{{ $t('app.pullImageHelper') }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<PushtoNode
|
||||
v-if="isMaster && isMasterProductPro && batchInstallSupport"
|
||||
:push-node="formData.pushNode"
|
||||
:nodes="formData.nodes"
|
||||
type="app"
|
||||
@update:push-node="formData.pushNode = $event"
|
||||
@update:nodes="formData.nodes = $event"
|
||||
/>
|
||||
|
||||
<el-form-item prop="editCompose">
|
||||
<el-checkbox v-model="formData.editCompose" :label="$t('app.editCompose')" size="large" />
|
||||
<span class="input-help">{{ $t('app.editComposeHelper') }}</span>
|
||||
|
|
@ -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: '<div></div>' };
|
||||
});
|
||||
|
||||
interface ClusterProps {
|
||||
key: string;
|
||||
|
|
@ -161,6 +179,7 @@ interface ClusterProps {
|
|||
interface Props {
|
||||
loading?: boolean;
|
||||
modelValue?: any;
|
||||
batchInstallSupport?: boolean;
|
||||
}
|
||||
|
||||
const limits = ref<Container.ResourceLimit>({
|
||||
|
|
@ -170,6 +189,7 @@ const limits = ref<Container.ResourceLimit>({
|
|||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
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());
|
||||
|
|
|
|||
|
|
@ -66,22 +66,22 @@
|
|||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
<MdEditor previewOnly v-model="app.readMe" :theme="isDarkTheme ? 'dark' : 'light'" />
|
||||
<MarkDownEditor :content="app.readMe" />
|
||||
</template>
|
||||
</DrawerPro>
|
||||
<Install ref="installRef" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MarkDownEditor from '@/components/mkdown-editor/index.vue';
|
||||
|
||||
import { getAppByKey, getAppDetail, getAppIconUrl } from '@/api/modules/app';
|
||||
import MdEditor from 'md-editor-v3';
|
||||
import { ref } from 'vue';
|
||||
import Install from './install/index.vue';
|
||||
import { computeSizeFromMB } from '@/utils/util';
|
||||
import { jumpToInstall } from '@/utils/app';
|
||||
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
const { currentNode, isDarkTheme } = useGlobalStore();
|
||||
const { currentNode } = useGlobalStore();
|
||||
|
||||
const app = ref<any>({});
|
||||
const appDetail = ref<any>({});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
<template>
|
||||
<DrawerPro v-model="open" :header="$t('commons.button.install')" @close="handleClose" size="large">
|
||||
<AppInstallForm ref="installFormRef" v-model="formData" :loading="loading" />
|
||||
<AppInstallForm
|
||||
ref="installFormRef"
|
||||
v-model="formData"
|
||||
:loading="loading"
|
||||
:batch-install-support="batchInstallSupport"
|
||||
/>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose" :disabled="loading">
|
||||
|
|
@ -24,12 +29,15 @@ import { newUUID } from '@/utils/util';
|
|||
import { routerToName } from '@/utils/router';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
import i18n from '@/lang';
|
||||
import { installAppToNodes } from '@/xpack/api/modules/appstore';
|
||||
|
||||
const router = useRouter();
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const installFormRef = ref<InstanceType<typeof AppInstallForm>>();
|
||||
const taskLogRef = ref();
|
||||
const appKey = ref('');
|
||||
const batchInstallSupport = ref(false);
|
||||
|
||||
const formData = ref({
|
||||
appDetailId: 0,
|
||||
|
|
@ -98,7 +106,12 @@ const install = async (submitData: any) => {
|
|||
submitData.taskID = taskID;
|
||||
|
||||
try {
|
||||
await installApp(submitData);
|
||||
if (submitData.pushNode) {
|
||||
submitData.appKey = appKey.value;
|
||||
await installAppToNodes(submitData);
|
||||
} else {
|
||||
await installApp(submitData);
|
||||
}
|
||||
handleClose();
|
||||
openTaskLog(taskID);
|
||||
} catch (error) {
|
||||
|
|
@ -112,6 +125,8 @@ const openTaskLog = (taskID: string) => {
|
|||
};
|
||||
|
||||
const acceptParams = async (props: { app: any; params?: any }) => {
|
||||
appKey.value = props.app.key;
|
||||
batchInstallSupport.value = props.app.batchInstallSupport;
|
||||
open.value = true;
|
||||
await nextTick();
|
||||
installFormRef.value?.resetForm();
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@
|
|||
v-if="isMaster && isMasterProductPro"
|
||||
:push-node="ssl.pushNode"
|
||||
:nodes="ssl.pushNodes"
|
||||
type="ssl"
|
||||
@update:push-node="ssl.pushNode = $event"
|
||||
@update:nodes="ssl.pushNodes = $event"
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue