mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-18 22:22:59 +08:00
feat: 应用安装页面 已安装应用列表
This commit is contained in:
parent
da85e416ea
commit
f7263934f9
@ -3,7 +3,7 @@ version: '3'
|
||||
services:
|
||||
mysql5.7:
|
||||
image: mysql:5.7.39
|
||||
container_name: 1panel-mysql
|
||||
container_name: ${CONTAINER_NAME}
|
||||
restart: always
|
||||
environment:
|
||||
TZ: ${TZ}
|
||||
|
@ -1,52 +0,0 @@
|
||||
{
|
||||
"form_fields": [
|
||||
{
|
||||
"type": "text",
|
||||
"label_zh": "时区",
|
||||
"label_en": "TimeZone",
|
||||
"required": true,
|
||||
"default": "Asia/Shanghai",
|
||||
"env_variable": "TZ"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label_zh": "数据库",
|
||||
"label_en": "Database",
|
||||
"required": true,
|
||||
"default": "db",
|
||||
"env_variable": "DATABASE"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label_zh": "普通用户",
|
||||
"label_en": "User",
|
||||
"required": true,
|
||||
"default": "mysql",
|
||||
"env_variable": "USER"
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"label_zh": "普通用户密码",
|
||||
"label_en": "Password",
|
||||
"required": true,
|
||||
"default": "1qaz@WSX",
|
||||
"env_variable": "PASSWORD"
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"label_zh": "Root用户密码",
|
||||
"label_en": "RootPassword",
|
||||
"required": true,
|
||||
"default": "1panel@mysql",
|
||||
"env_variable": "ROOT_PASSWORD"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"label_zh": "端口",
|
||||
"label_en": "Port",
|
||||
"required": true,
|
||||
"default": 3306,
|
||||
"env_variable": "PORT"
|
||||
}
|
||||
]
|
||||
}
|
52
apps/mysql/5.7.39/params.json
Normal file
52
apps/mysql/5.7.39/params.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"formFields": [
|
||||
{
|
||||
"type": "text",
|
||||
"labelZh": "时区",
|
||||
"labelEn": "TimeZone",
|
||||
"required": true,
|
||||
"default": "Asia/Shanghai",
|
||||
"envKey": "TZ"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"labelZh": "数据库",
|
||||
"labelEn": "Database",
|
||||
"required": true,
|
||||
"default": "db",
|
||||
"envKey": "DATABASE"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"labelZh": "普通用户",
|
||||
"labelEn": "User",
|
||||
"required": true,
|
||||
"default": "mysql",
|
||||
"envKey": "USER"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"labelZh": "普通用户密码",
|
||||
"labelEn": "Password",
|
||||
"required": true,
|
||||
"default": "1qaz@WSX",
|
||||
"envKey": "PASSWORD"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"labelZh": "Root用户密码",
|
||||
"labelEn": "RootPassword",
|
||||
"required": true,
|
||||
"default": "1panel@mysql",
|
||||
"envKey": "ROOT_PASSWORD"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"labelZh": "端口",
|
||||
"labelEn": "Port",
|
||||
"required": true,
|
||||
"default": 3306,
|
||||
"envKey": "PORT"
|
||||
}
|
||||
]
|
||||
}
|
@ -61,3 +61,34 @@ func (b *BaseApi) GetAppDetail(c *gin.Context) {
|
||||
}
|
||||
helper.SuccessWithData(c, appDetailDTO)
|
||||
}
|
||||
|
||||
func (b *BaseApi) InstallApp(c *gin.Context) {
|
||||
var req dto.AppInstallRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||
return
|
||||
}
|
||||
if err := appService.Install(req.AppDetailId, req.Params); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
func (b *BaseApi) PageInstalled(c *gin.Context) {
|
||||
var req dto.AppInstalledRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||
return
|
||||
}
|
||||
total, list, err := appService.PageInstalled(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Items: list,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ type AppDTO struct {
|
||||
|
||||
type AppDetailDTO struct {
|
||||
model.AppDetail
|
||||
Params interface{} `json:"params"`
|
||||
}
|
||||
|
||||
type AppList struct {
|
||||
@ -61,3 +62,20 @@ type AppRequest struct {
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type AppInstallRequest struct {
|
||||
AppDetailId uint `json:"appDetailId" validate:"required"`
|
||||
Params map[string]interface{} `json:"params"`
|
||||
}
|
||||
|
||||
type AppInstalled struct {
|
||||
model.AppInstall
|
||||
Total int `json:"total"`
|
||||
Ready int `json:"ready"`
|
||||
AppName string `json:"appName"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
type AppInstalledRequest struct {
|
||||
PageInfo
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ type AppDetail struct {
|
||||
BaseModel
|
||||
AppId uint `json:"appId" gorm:"type:integer;not null"`
|
||||
Version string `json:"version" gorm:"type:varchar(64);not null"`
|
||||
FormFields string `json:"formFields" gorm:"type:longtext;"`
|
||||
DockerCompose string `json:"dockerCompose" gorm:"type:longtext;not null"`
|
||||
Params string `json:"-" gorm:"type:longtext;"`
|
||||
DockerCompose string `json:"-" gorm:"type:longtext;not null"`
|
||||
Readme string `json:"readme" gorm:"type:longtext;not null"`
|
||||
}
|
||||
|
14
backend/app/model/app_install.go
Normal file
14
backend/app/model/app_install.go
Normal file
@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
type AppInstall struct {
|
||||
BaseModel
|
||||
ContainerName string `json:"containerName" gorm:"type:varchar(256);not null"`
|
||||
Version string `json:"version" gorm:"type:varchar(256);not null"`
|
||||
AppId uint `json:"appId" gorm:"type:integer;not null"`
|
||||
AppDetailId uint `json:"appDetailId" gorm:"type:integer;not null"`
|
||||
Params string `json:"params" gorm:"type:longtext;not null"`
|
||||
Status string `json:"status" gorm:"type:varchar(256);not null"`
|
||||
Description string `json:"description" gorm:"type:varchar(256);not null"`
|
||||
Message string `json:"message" gorm:"type:longtext;not null"`
|
||||
App App `json:"-"`
|
||||
}
|
39
backend/app/repo/app_install.go
Normal file
39
backend/app/repo/app_install.go
Normal file
@ -0,0 +1,39 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/app/model"
|
||||
"github.com/1Panel-dev/1Panel/global"
|
||||
)
|
||||
|
||||
type AppInstallRepo struct{}
|
||||
|
||||
func (a AppInstallRepo) GetBy(opts ...DBOption) ([]model.AppInstall, error) {
|
||||
db := global.DB.Model(&model.AppInstall{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
var install []model.AppInstall
|
||||
err := db.Find(&install).Error
|
||||
return install, err
|
||||
}
|
||||
|
||||
func (a AppInstallRepo) Create(install model.AppInstall) error {
|
||||
db := global.DB.Model(&model.AppInstall{})
|
||||
return db.Create(&install).Error
|
||||
}
|
||||
func (a AppInstallRepo) Save(install model.AppInstall) error {
|
||||
db := global.DB.Model(&model.AppInstall{})
|
||||
return db.Save(&install).Error
|
||||
}
|
||||
|
||||
func (a AppInstallRepo) Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error) {
|
||||
var apps []model.AppInstall
|
||||
db := global.DB.Model(&model.AppInstall{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
count := int64(0)
|
||||
db = db.Count(&count)
|
||||
err := db.Debug().Limit(size).Offset(size * (page - 1)).Preload("App").Find(&apps).Error
|
||||
return count, apps, err
|
||||
}
|
@ -13,6 +13,7 @@ type RepoGroup struct {
|
||||
AppTagRepo
|
||||
TagRepo
|
||||
AppDetailRepo
|
||||
AppInstallRepo
|
||||
}
|
||||
|
||||
var RepoGroupApp = new(RepoGroup)
|
||||
|
@ -6,13 +6,18 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/app/model"
|
||||
"github.com/1Panel-dev/1Panel/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/constant"
|
||||
"github.com/1Panel-dev/1Panel/global"
|
||||
"github.com/1Panel-dev/1Panel/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/utils/compose"
|
||||
"github.com/1Panel-dev/1Panel/utils/files"
|
||||
"github.com/joho/godotenv"
|
||||
"golang.org/x/net/context"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type AppService struct {
|
||||
@ -105,6 +110,24 @@ func (a AppService) GetApp(id uint) (dto.AppDTO, error) {
|
||||
return appDTO, nil
|
||||
}
|
||||
|
||||
func (a AppService) PageInstalled(req dto.AppInstalledRequest) (int64, []dto.AppInstalled, error) {
|
||||
total, installed, err := appInstallRepo.Page(req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
installDTOs := []dto.AppInstalled{}
|
||||
for _, in := range installed {
|
||||
installDto := dto.AppInstalled{
|
||||
AppInstall: in,
|
||||
AppName: in.App.Name,
|
||||
Icon: in.App.Icon,
|
||||
}
|
||||
installDTOs = append(installDTOs, installDto)
|
||||
}
|
||||
|
||||
return total, installDTOs, nil
|
||||
}
|
||||
|
||||
func (a AppService) GetAppDetail(appId uint, version string) (dto.AppDetailDTO, error) {
|
||||
|
||||
var (
|
||||
@ -117,11 +140,85 @@ func (a AppService) GetAppDetail(appId uint, version string) (dto.AppDetailDTO,
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
|
||||
paramMap := make(map[string]interface{})
|
||||
json.Unmarshal([]byte(detail.Params), ¶mMap)
|
||||
appDetailDTO.AppDetail = detail
|
||||
appDetailDTO.Params = paramMap
|
||||
return appDetailDTO, nil
|
||||
}
|
||||
|
||||
func (a AppService) Install(appDetailId uint, params map[string]interface{}) error {
|
||||
appDetail, err := appDetailRepo.GetAppDetail(commonRepo.WithByID(appDetailId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
app, err := appRepo.GetFirst(commonRepo.WithByID(appDetail.AppId))
|
||||
paramByte, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
containerName := constant.ContainerPrefix + app.Key + "-" + common.RandStr(6)
|
||||
appInstall := model.AppInstall{
|
||||
AppId: appDetail.AppId,
|
||||
AppDetailId: appDetail.ID,
|
||||
Version: appDetail.Version,
|
||||
Status: constant.Installing,
|
||||
Params: string(paramByte),
|
||||
ContainerName: containerName,
|
||||
Message: "",
|
||||
}
|
||||
resourceDir := path.Join(global.CONF.System.ResourceDir, "apps", app.Key, appDetail.Version)
|
||||
installDir := path.Join(global.CONF.System.AppDir, app.Key)
|
||||
op := files.NewFileOp()
|
||||
if err := op.CopyDir(resourceDir, installDir); err != nil {
|
||||
return err
|
||||
}
|
||||
installAppDir := path.Join(installDir, appDetail.Version)
|
||||
containerNameDir := path.Join(installDir, containerName)
|
||||
if err := op.Rename(installAppDir, containerNameDir); err != nil {
|
||||
return err
|
||||
}
|
||||
composeFilePath := path.Join(containerNameDir, "docker-compose.yml")
|
||||
envPath := path.Join(containerNameDir, ".env")
|
||||
|
||||
envParams := make(map[string]string, len(params))
|
||||
for k, v := range params {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
envParams[k] = t
|
||||
case float64:
|
||||
envParams[k] = strconv.FormatFloat(t, 'f', -1, 32)
|
||||
default:
|
||||
envParams[k] = t.(string)
|
||||
}
|
||||
}
|
||||
envParams["CONTAINER_NAME"] = containerName
|
||||
if err := godotenv.Write(envParams, envPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appInstallRepo.Create(appInstall); err != nil {
|
||||
return err
|
||||
}
|
||||
go upApp(composeFilePath, appInstall)
|
||||
return nil
|
||||
}
|
||||
|
||||
func upApp(composeFilePath string, appInstall model.AppInstall) {
|
||||
out, err := compose.Up(composeFilePath)
|
||||
if err != nil {
|
||||
if out != "" {
|
||||
appInstall.Message = out
|
||||
} else {
|
||||
appInstall.Message = err.Error()
|
||||
}
|
||||
appInstall.Status = constant.Error
|
||||
_ = appInstallRepo.Save(appInstall)
|
||||
} else {
|
||||
appInstall.Status = constant.Running
|
||||
_ = appInstallRepo.Save(appInstall)
|
||||
}
|
||||
}
|
||||
|
||||
func (a AppService) Sync() error {
|
||||
//TODO 从 oss 拉取最新列表
|
||||
var appConfig model.AppConfig
|
||||
@ -204,11 +301,11 @@ func (a AppService) Sync() error {
|
||||
continue
|
||||
}
|
||||
detail.DockerCompose = string(dockerComposeStr)
|
||||
formStr, err := os.ReadFile(path.Join(detailPath, "form.json"))
|
||||
paramStr, err := os.ReadFile(path.Join(detailPath, "params.json"))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("get [%s] form.json error: %s", detailPath, err.Error())
|
||||
}
|
||||
detail.FormFields = string(formStr)
|
||||
detail.Params = string(paramStr)
|
||||
app.Details = append(app.Details, detail)
|
||||
}
|
||||
}
|
||||
|
@ -30,4 +30,5 @@ var (
|
||||
appTagRepo = repo.RepoGroupApp.AppTagRepo
|
||||
appDetailRepo = repo.RepoGroupApp.AppDetailRepo
|
||||
tagRepo = repo.RepoGroupApp.TagRepo
|
||||
appInstallRepo = repo.AppInstallRepo{}
|
||||
)
|
||||
|
@ -1,14 +1,13 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/global"
|
||||
"github.com/1Panel-dev/1Panel/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/utils/files"
|
||||
"github.com/pkg/errors"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -36,14 +35,14 @@ func (f FileService) GetFileTree(op dto.FileOption) ([]dto.FileTree, error) {
|
||||
return nil, err
|
||||
}
|
||||
node := dto.FileTree{
|
||||
ID: getUuid(),
|
||||
ID: common.GetUuid(),
|
||||
Name: info.Name,
|
||||
Path: info.Path,
|
||||
}
|
||||
for _, v := range info.Items {
|
||||
if v.IsDir {
|
||||
node.Children = append(node.Children, dto.FileTree{
|
||||
ID: getUuid(),
|
||||
ID: common.GetUuid(),
|
||||
Name: v.Name,
|
||||
Path: v.Path,
|
||||
})
|
||||
@ -177,11 +176,3 @@ func (f FileService) DirSize(req dto.DirSizeReq) (dto.DirSizeRes, error) {
|
||||
}
|
||||
return dto.DirSizeRes{Size: size}, nil
|
||||
}
|
||||
|
||||
func getUuid() string {
|
||||
b := make([]byte, 16)
|
||||
_, _ = io.ReadFull(rand.Reader, b)
|
||||
b[6] = (b[6] & 0x0f) | 0x40
|
||||
b[8] = (b[8] & 0x3f) | 0x80
|
||||
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
|
||||
}
|
||||
|
13
backend/constant/app.go
Normal file
13
backend/constant/app.go
Normal file
@ -0,0 +1,13 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
Running = "Running"
|
||||
Warning = "Warning"
|
||||
Error = "Error"
|
||||
Stopped = "Stopped"
|
||||
Installing = "Installing"
|
||||
)
|
||||
|
||||
const (
|
||||
ContainerPrefix = "1Panel-"
|
||||
)
|
@ -276,6 +276,7 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
|
@ -150,6 +150,6 @@ var AddTableCronjob = &gormigrate.Migration{
|
||||
var AddTableApp = &gormigrate.Migration{
|
||||
ID: "20200921-add-table-app",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(&model.App{}, &model.AppDetail{}, &model.Tag{}, &model.AppTag{}, &model.AppConfig{})
|
||||
return tx.AutoMigrate(&model.App{}, &model.AppDetail{}, &model.Tag{}, &model.AppTag{}, &model.AppConfig{}, &model.AppInstall{})
|
||||
},
|
||||
}
|
||||
|
@ -19,5 +19,7 @@ func (a *AppRouter) InitAppRouter(Router *gin.RouterGroup) {
|
||||
appRouter.POST("/search", baseApi.AppSearch)
|
||||
appRouter.GET("/:id", baseApi.GetApp)
|
||||
appRouter.GET("/detail/:appid/:version", baseApi.GetAppDetail)
|
||||
appRouter.POST("/install", baseApi.InstallApp)
|
||||
appRouter.POST("/installed", baseApi.PageInstalled)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
mathRand "math/rand"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -38,3 +42,21 @@ func min(a, b int) int {
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func GetUuid() string {
|
||||
b := make([]byte, 16)
|
||||
_, _ = io.ReadFull(rand.Reader, b)
|
||||
b[6] = (b[6] & 0x0f) | 0x40
|
||||
b[8] = (b[8] & 0x3f) | 0x80
|
||||
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
|
||||
}
|
||||
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
|
||||
|
||||
func RandStr(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letters[mathRand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ func Up(filePath string) (string, error) {
|
||||
cmd := exec.Command("docker-compose", "-f", filePath, "up", "-d")
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
return string(stdout), err
|
||||
}
|
||||
return string(stdout), nil
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ export namespace App {
|
||||
icon: string;
|
||||
version: string;
|
||||
readme: string;
|
||||
formFields: string;
|
||||
params: AppParams;
|
||||
dockerCompose: string;
|
||||
}
|
||||
|
||||
@ -42,4 +42,37 @@ export namespace App {
|
||||
name: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface AppParams {
|
||||
formFields: FromField[];
|
||||
}
|
||||
|
||||
export interface FromField {
|
||||
type: string;
|
||||
labelZh: string;
|
||||
labelEn: string;
|
||||
required: boolean;
|
||||
default: any;
|
||||
envKey: string;
|
||||
}
|
||||
|
||||
export interface AppInstall {
|
||||
appDetailId: number;
|
||||
params: any;
|
||||
}
|
||||
|
||||
export interface AppInstalled extends CommonModel {
|
||||
containerName: string;
|
||||
version: string;
|
||||
appId: string;
|
||||
appDetailId: string;
|
||||
params: string;
|
||||
status: string;
|
||||
description: string;
|
||||
message: string;
|
||||
appName: string;
|
||||
total: number;
|
||||
ready: number;
|
||||
icon: string;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import http from '@/api';
|
||||
import { ReqPage, ResPage } from '../interface';
|
||||
import { App } from '../interface/app';
|
||||
|
||||
export const SyncApp = () => {
|
||||
@ -16,3 +17,11 @@ export const GetApp = (id: number) => {
|
||||
export const GetAppDetail = (id: number, version: string) => {
|
||||
return http.get<App.AppDetail>('apps/detail/' + id + '/' + version);
|
||||
};
|
||||
|
||||
export const InstallApp = (install: App.AppInstall) => {
|
||||
return http.post<any>('apps/install', install);
|
||||
};
|
||||
|
||||
export const GetAppInstalled = (info: ReqPage) => {
|
||||
return http.post<ResPage<App.AppInstalled>>('apps/installed', info);
|
||||
};
|
||||
|
@ -22,7 +22,7 @@
|
||||
</template>
|
||||
|
||||
<div class="complex-table__body">
|
||||
<fu-table v-bind="$attrs" @selection-change="handleSelectionChange" height="67vh">
|
||||
<fu-table v-bind="$attrs" @selection-change="handleSelectionChange" max-height="67vh">
|
||||
<slot></slot>
|
||||
</fu-table>
|
||||
</div>
|
||||
|
@ -394,5 +394,14 @@ export default {
|
||||
version: '版本',
|
||||
detail: '详情',
|
||||
install: '安装',
|
||||
author: '作者',
|
||||
source: '来源',
|
||||
sync: '同步',
|
||||
appName: '应用名称',
|
||||
status: '状态',
|
||||
container: '容器',
|
||||
restart: '重启',
|
||||
up: '启动',
|
||||
down: '停止',
|
||||
},
|
||||
};
|
||||
|
137
frontend/src/views/app-store/apps/index.vue
Normal file
137
frontend/src/views/app-store/apps/index.vue
Normal file
@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-input v-model="req.name" @blur="searchByName"></el-input>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-select v-model="req.tags" multiple style="width: 100%" @change="changeTag">
|
||||
<el-option v-for="item in tags" :key="item.key" :label="item.name" :value="item.key"></el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col v-for="(app, index) in apps" :key="index" :xs="8" :sm="8" :lg="4">
|
||||
<div @click="getAppDetail(app.id)">
|
||||
<el-card :body-style="{ padding: '0px' }" class="a-card">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<div class="icon">
|
||||
<el-image class="image" :src="'data:image/png;base64,' + app.icon"></el-image>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<div class="a-detail">
|
||||
<div class="d-name">
|
||||
<font size="3" style="font-weight: 700">{{ app.name }}</font>
|
||||
</div>
|
||||
<div class="d-description">
|
||||
<font size="1">
|
||||
<span>
|
||||
{{ app.shortDesc }}
|
||||
</span>
|
||||
</font>
|
||||
</div>
|
||||
<div class="d-tag">
|
||||
<el-tag v-for="(tag, ind) in app.tags" :key="ind" round :colr="getColor(ind)">
|
||||
{{ tag.name }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { App } from '@/api/interface/app';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import router from '@/routers';
|
||||
import { SearchApp } from '@/api/modules/app';
|
||||
|
||||
let req = reactive({
|
||||
name: '',
|
||||
tags: [],
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
});
|
||||
|
||||
let apps = ref<App.App[]>([]);
|
||||
let tags = ref<App.Tag[]>([]);
|
||||
const colorArr = ['#6495ED', '#54FF9F', '#BEBEBE', '#FFF68F', '#FFFF00', '#8B0000'];
|
||||
|
||||
const getColor = (index: number) => {
|
||||
return colorArr[index];
|
||||
};
|
||||
|
||||
const search = async (req: App.AppReq) => {
|
||||
await SearchApp(req).then((res) => {
|
||||
apps.value = res.data.items;
|
||||
tags.value = res.data.tags;
|
||||
});
|
||||
};
|
||||
|
||||
const getAppDetail = (id: number) => {
|
||||
let params: { [key: string]: any } = {
|
||||
id: id,
|
||||
};
|
||||
router.push({ name: 'AppDetail', params });
|
||||
};
|
||||
|
||||
const changeTag = () => {
|
||||
search(req);
|
||||
};
|
||||
|
||||
const searchByName = () => {
|
||||
search(req);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
search(req);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.header {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.a-card {
|
||||
height: 100px;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
padding: 1px;
|
||||
|
||||
.icon {
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
padding: 10%;
|
||||
margin-top: 5px;
|
||||
.image {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.a-detail {
|
||||
margin-top: 10px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.d-name {
|
||||
height: 20%;
|
||||
}
|
||||
|
||||
.d-description {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.a-card:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
</style>
|
@ -28,15 +28,15 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="'来源'">
|
||||
<el-descriptions-item :label="$t('app.source')">
|
||||
<el-link @click="toLink(app.source)">
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="'作者'">{{ app.author }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('app.author')">{{ app.author }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div>
|
||||
<el-button type="primary">{{ $t('app.install') }}</el-button>
|
||||
<el-button @click="openInstall" type="primary">{{ $t('app.install') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
@ -46,6 +46,7 @@
|
||||
<div class="detail" v-loading="loadingDetail">
|
||||
<v-md-preview :text="appDetail.readme"></v-md-preview>
|
||||
</div>
|
||||
<Install ref="installRef"></Install>
|
||||
</LayoutContent>
|
||||
</template>
|
||||
|
||||
@ -53,6 +54,7 @@
|
||||
import { GetApp, GetAppDetail } from '@/api/modules/app';
|
||||
import LayoutContent from '@/layout/layout-content.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import Install from './install.vue';
|
||||
|
||||
interface OperateProps {
|
||||
id: number;
|
||||
@ -64,6 +66,7 @@ let app = ref<any>({});
|
||||
let appDetail = ref<any>({});
|
||||
let version = ref('');
|
||||
let loadingDetail = ref(false);
|
||||
const installRef = ref();
|
||||
|
||||
const getApp = () => {
|
||||
GetApp(props.id).then((res) => {
|
||||
@ -88,6 +91,14 @@ const toLink = (link: string) => {
|
||||
window.open(link, '_blank');
|
||||
};
|
||||
|
||||
const openInstall = () => {
|
||||
let params = {
|
||||
params: appDetail.value.params,
|
||||
appDetailId: appDetail.value.id,
|
||||
};
|
||||
installRef.value.acceptParams(params);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getApp();
|
||||
});
|
||||
|
94
frontend/src/views/app-store/detail/install.vue
Normal file
94
frontend/src/views/app-store/detail/install.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<el-dialog v-model="open" :title="$t('app.install')" width="30%">
|
||||
<el-form ref="paramForm" label-position="left" :model="form" label-width="150px" :rules="rules">
|
||||
<div v-for="(f, index) in installData.params?.formFields" :key="index">
|
||||
<el-form-item :label="f.labelZh" :prop="f.envKey">
|
||||
<el-input
|
||||
v-model="form[f.envKey]"
|
||||
v-if="f.type == 'text' || f.type == 'number'"
|
||||
:type="f.type"
|
||||
></el-input>
|
||||
<el-input
|
||||
v-model="form[f.envKey]"
|
||||
v-if="f.type == 'password'"
|
||||
:type="f.type"
|
||||
show-password
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="submit(paramForm)" :disabled="loading">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="appInstall">
|
||||
import { App } from '@/api/interface/app';
|
||||
import { InstallApp } from '@/api/modules/app';
|
||||
import { Rules } from '@/global/form-rues';
|
||||
import { FormInstance, FormRules } from 'element-plus';
|
||||
import { reactive, ref } from 'vue';
|
||||
interface InstallRrops {
|
||||
appDetailId: number;
|
||||
params?: App.AppParams;
|
||||
}
|
||||
|
||||
const installData = ref<InstallRrops>({
|
||||
appDetailId: 0,
|
||||
});
|
||||
let open = ref(false);
|
||||
let form = reactive<{ [key: string]: any }>({});
|
||||
let rules = reactive<FormRules>({});
|
||||
let loading = false;
|
||||
const paramForm = ref<FormInstance>();
|
||||
const req = reactive({
|
||||
appDetailId: 0,
|
||||
params: {},
|
||||
});
|
||||
const em = defineEmits(['close']);
|
||||
const handleClose = () => {
|
||||
if (paramForm.value) {
|
||||
paramForm.value.resetFields();
|
||||
}
|
||||
open.value = false;
|
||||
em('close', open);
|
||||
};
|
||||
|
||||
const acceptParams = (props: InstallRrops): void => {
|
||||
installData.value = props;
|
||||
const params = installData.value.params;
|
||||
if (params?.formFields != undefined) {
|
||||
for (const p of params?.formFields) {
|
||||
form[p.envKey] = p.default;
|
||||
if (p.required) {
|
||||
rules[p.envKey] = [Rules.requiredInput];
|
||||
}
|
||||
}
|
||||
}
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
await formEl.validate((valid) => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
req.appDetailId = installData.value.appDetailId;
|
||||
req.params = form;
|
||||
InstallApp(req).then((res) => {
|
||||
console.log(res);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
@ -1,108 +1,40 @@
|
||||
<template>
|
||||
<LayoutContent>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-input v-model="req.name" @blur="searchByName"></el-input>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-select v-model="req.tags" multiple style="width: 100%" @change="changeTag">
|
||||
<el-option v-for="item in tags" :key="item.key" :label="item.name" :value="item.key"></el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<!-- <el-button @click="sync">同步</el-button> -->
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col v-for="(app, index) in apps" :key="index" :xs="8" :sm="8" :lg="4">
|
||||
<div @click="getAppDetail(app.id)">
|
||||
<el-card :body-style="{ padding: '0px' }" class="a-card">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<div class="icon">
|
||||
<el-image class="image" :src="'data:image/png;base64,' + app.icon"></el-image>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<div class="a-detail">
|
||||
<div class="d-name">
|
||||
<font size="3" style="font-weight: 700">{{ app.name }}</font>
|
||||
</div>
|
||||
<div class="d-description">
|
||||
<font size="1">
|
||||
<span>
|
||||
{{ app.shortDesc }}
|
||||
</span>
|
||||
</font>
|
||||
</div>
|
||||
<div class="d-tag">
|
||||
<el-tag v-for="(tag, ind) in app.tags" :key="ind" round :colr="getColor(ind)">
|
||||
{{ tag.name }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-col :span="24">
|
||||
<div style="margin-bottom: 10px">
|
||||
<el-radio-group v-model="activeName">
|
||||
<el-radio-button label="all">
|
||||
{{ $t('app.all') }}
|
||||
</el-radio-button>
|
||||
<el-radio-button label="installed">
|
||||
{{ $t('app.installed') }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div style="float: right">
|
||||
<el-button @click="sync">{{ $t('app.sync') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<Apps v-if="activeName === 'all'"></Apps>
|
||||
<Installed v-if="activeName === 'installed'"></Installed>
|
||||
</LayoutContent>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { App } from '@/api/interface/app';
|
||||
import LayoutContent from '@/layout/layout-content.vue';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import router from '@/routers';
|
||||
// import { SyncApp } from '@/api/modules/app';
|
||||
import { SearchApp } from '@/api/modules/app';
|
||||
import { ref } from 'vue';
|
||||
import { SyncApp } from '@/api/modules/app';
|
||||
import Apps from './apps/index.vue';
|
||||
import Installed from './installed/index.vue';
|
||||
const activeName = ref('all');
|
||||
|
||||
let req = reactive({
|
||||
name: '',
|
||||
tags: [],
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
});
|
||||
|
||||
let apps = ref<App.App[]>([]);
|
||||
let tags = ref<App.Tag[]>([]);
|
||||
const colorArr = ['#6495ED', '#54FF9F', '#BEBEBE', '#FFF68F', '#FFFF00', '#8B0000'];
|
||||
|
||||
const getColor = (index: number) => {
|
||||
return colorArr[index];
|
||||
};
|
||||
|
||||
// const sync = () => {
|
||||
// SyncApp().then((res) => {
|
||||
// console.log(res);
|
||||
// });
|
||||
// };
|
||||
|
||||
const search = async (req: App.AppReq) => {
|
||||
await SearchApp(req).then((res) => {
|
||||
apps.value = res.data.items;
|
||||
tags.value = res.data.tags;
|
||||
const sync = () => {
|
||||
SyncApp().then((res) => {
|
||||
console.log(res);
|
||||
});
|
||||
};
|
||||
|
||||
const getAppDetail = (id: number) => {
|
||||
console.log(id);
|
||||
let params: { [key: string]: any } = {
|
||||
id: id,
|
||||
};
|
||||
router.push({ name: 'AppDetail', params });
|
||||
};
|
||||
|
||||
const changeTag = () => {
|
||||
search(req);
|
||||
};
|
||||
|
||||
const searchByName = () => {
|
||||
search(req);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
search(req);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
85
frontend/src/views/app-store/installed/index.vue
Normal file
85
frontend/src/views/app-store/installed/index.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search">
|
||||
<el-table-column :label="$t('app.appName')" prop="appName"></el-table-column>
|
||||
<el-table-column :label="$t('app.version')" prop="version"></el-table-column>
|
||||
<el-table-column :label="$t('app.container')">
|
||||
<template #default="{ row }">
|
||||
{{ row.ready / row.total }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('app.status')">
|
||||
<template #default="{ row }">
|
||||
<el-tag>{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
:label="$t('commons.table.date')"
|
||||
:formatter="dateFromat"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<fu-table-operations :ellipsis="10" :buttons="buttons" :label="$t('commons.table.operate')" fixed="right" fix />
|
||||
</ComplexTable>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { GetAppInstalled } from '@/api/modules/app';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import ComplexTable from '@/components/complex-table/index.vue';
|
||||
import { dateFromat } from '@/utils/util';
|
||||
import i18n from '@/lang';
|
||||
|
||||
let data = ref<any>();
|
||||
|
||||
const paginationConfig = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const search = () => {
|
||||
const req = {
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
};
|
||||
|
||||
GetAppInstalled(req).then((res) => {
|
||||
data.value = res.data.items;
|
||||
paginationConfig.total = res.data.total;
|
||||
});
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('app.restart'),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('app.up'),
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('app.down'),
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
search();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.i-card {
|
||||
height: 60px;
|
||||
cursor: pointer;
|
||||
.content {
|
||||
.image {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
.i-card:hover {
|
||||
border: 1px solid;
|
||||
border-color: $primary-color;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
@ -64,7 +64,6 @@ const initProcess = () => {
|
||||
|
||||
const getKeys = () => {
|
||||
FileKeys().then((res) => {
|
||||
console.log(res);
|
||||
if (res.data.keys.length > 0) {
|
||||
keys.value = res.data.keys;
|
||||
initProcess();
|
||||
|
Loading…
Reference in New Issue
Block a user