feat: 运行环境增加 Node.js 管理 (#2390)

Refs https://github.com/1Panel-dev/1Panel/issues/397
This commit is contained in:
zhengkunwang 2023-09-25 17:50:14 +08:00 committed by GitHub
parent 38dadf6056
commit 1130a70052
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1974 additions and 396 deletions

View File

@ -70,7 +70,7 @@ func (b *BaseApi) DeleteRuntime(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
err := runtimeService.Delete(req.ID)
err := runtimeService.Delete(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
@ -121,3 +121,25 @@ func (b *BaseApi) GetRuntime(c *gin.Context) {
}
helper.SuccessWithData(c, res)
}
// @Tags Runtime
// @Summary Get Node package scripts
// @Description 获取 Node 项目的 scripts
// @Accept json
// @Param request body request.NodePackageReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /runtimes/node/package [post]
func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
var req request.NodePackageReq
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
res, err := runtimeService.GetNodePackageRunScript(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, res)
}

View File

@ -18,10 +18,18 @@ type RuntimeCreate struct {
Type string `json:"type"`
Version string `json:"version"`
Source string `json:"source"`
CodeDir string `json:"codeDir"`
NodeConfig
}
type NodeConfig struct {
Install bool `json:"install"`
Clean bool `json:"clean"`
}
type RuntimeDelete struct {
ID uint `json:"id"`
ID uint `json:"id"`
ForceDelete bool `json:"forceDelete"`
}
type RuntimeUpdate struct {
@ -32,4 +40,10 @@ type RuntimeUpdate struct {
Version string `json:"version"`
Rebuild bool `json:"rebuild"`
Source string `json:"source"`
CodeDir string `json:"codeDir"`
NodeConfig
}
type NodePackageReq struct {
CodeDir string `json:"codeDir"`
}

View File

@ -1,10 +1,45 @@
package response
import "github.com/1Panel-dev/1Panel/backend/app/model"
import (
"github.com/1Panel-dev/1Panel/backend/app/model"
"time"
)
type RuntimeRes struct {
model.Runtime
AppParams []AppParam `json:"appParams"`
AppID uint `json:"appId"`
Source string `json:"source"`
type RuntimeDTO struct {
ID uint `json:"id"`
Name string `json:"name"`
Resource string `json:"resource"`
AppDetailID uint `json:"appDetailID"`
AppID uint `json:"appID"`
Source string `json:"source"`
Status string `json:"status"`
Type string `json:"type"`
Image string `json:"image"`
Params map[string]interface{} `json:"params"`
Message string `json:"message"`
Version string `json:"version"`
CreatedAt time.Time `json:"createdAt"`
CodeDir string `json:"codeDir"`
AppParams []AppParam `json:"appParams"`
}
type PackageScripts struct {
Name string `json:"name"`
Script string `json:"script"`
}
func NewRuntimeDTO(runtime model.Runtime) RuntimeDTO {
return RuntimeDTO{
ID: runtime.ID,
Name: runtime.Name,
Resource: runtime.Resource,
AppDetailID: runtime.AppDetailID,
Status: runtime.Status,
Type: runtime.Type,
Image: runtime.Image,
Message: runtime.Message,
CreatedAt: runtime.CreatedAt,
CodeDir: runtime.CodeDir,
Version: runtime.Version,
}
}

View File

@ -1,5 +1,10 @@
package model
import (
"github.com/1Panel-dev/1Panel/backend/constant"
"path"
)
type Runtime struct {
BaseModel
Name string `gorm:"type:varchar;not null" json:"name"`
@ -14,4 +19,21 @@ type Runtime struct {
Status string `gorm:"type:varchar;not null" json:"status"`
Resource string `gorm:"type:varchar;not null" json:"resource"`
Message string `gorm:"type:longtext;" json:"message"`
CodeDir string `gorm:"type:varchar;" json:"codeDir"`
}
func (r *Runtime) GetComposePath() string {
return path.Join(r.GetPath(), "docker-compose.yml")
}
func (r *Runtime) GetEnvPath() string {
return path.Join(r.GetPath(), ".env")
}
func (r *Runtime) GetPath() string {
return path.Join(constant.RuntimeDir, r.Type, r.Name)
}
func (r *Runtime) GetLogPath() string {
return path.Join(r.GetPath(), "build.log")
}

View File

@ -176,36 +176,39 @@ func (a AppService) GetAppDetail(appId uint, version, appType string) (response.
return appDetailDTO, err
}
}
buildPath := path.Join(versionPath, "build")
paramsPath := path.Join(buildPath, "config.json")
if !fileOp.Stat(paramsPath) {
return appDetailDTO, buserr.New(constant.ErrFileNotExist)
}
param, err := fileOp.GetContent(paramsPath)
if err != nil {
return appDetailDTO, err
}
paramMap := make(map[string]interface{})
if err := json.Unmarshal(param, &paramMap); err != nil {
return appDetailDTO, err
}
appDetailDTO.Params = paramMap
composePath := path.Join(buildPath, "docker-compose.yml")
if !fileOp.Stat(composePath) {
return appDetailDTO, buserr.New(constant.ErrFileNotExist)
}
compose, err := fileOp.GetContent(composePath)
if err != nil {
return appDetailDTO, err
}
composeMap := make(map[string]interface{})
if err := yaml.Unmarshal(compose, &composeMap); err != nil {
return appDetailDTO, err
}
if service, ok := composeMap["services"]; ok {
servicesMap := service.(map[string]interface{})
for k := range servicesMap {
appDetailDTO.Image = k
switch app.Type {
case constant.RuntimePHP:
buildPath := path.Join(versionPath, "build")
paramsPath := path.Join(buildPath, "config.json")
if !fileOp.Stat(paramsPath) {
return appDetailDTO, buserr.New(constant.ErrFileNotExist)
}
param, err := fileOp.GetContent(paramsPath)
if err != nil {
return appDetailDTO, err
}
paramMap := make(map[string]interface{})
if err := json.Unmarshal(param, &paramMap); err != nil {
return appDetailDTO, err
}
appDetailDTO.Params = paramMap
composePath := path.Join(buildPath, "docker-compose.yml")
if !fileOp.Stat(composePath) {
return appDetailDTO, buserr.New(constant.ErrFileNotExist)
}
compose, err := fileOp.GetContent(composePath)
if err != nil {
return appDetailDTO, err
}
composeMap := make(map[string]interface{})
if err := yaml.Unmarshal(compose, &composeMap); err != nil {
return appDetailDTO, err
}
if service, ok := composeMap["services"]; ok {
servicesMap := service.(map[string]interface{})
for k := range servicesMap {
appDetailDTO.Image = k
}
}
}
} else {

View File

@ -3,7 +3,6 @@ package service
import (
"context"
"encoding/json"
"fmt"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/app/dto/response"
@ -12,24 +11,26 @@ import (
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/compose"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/pkg/errors"
"github.com/subosito/gotenv"
"path"
"path/filepath"
"strconv"
"strings"
"time"
)
type RuntimeService struct {
}
type IRuntimeService interface {
Page(req request.RuntimeSearch) (int64, []response.RuntimeRes, error)
Page(req request.RuntimeSearch) (int64, []response.RuntimeDTO, error)
Create(create request.RuntimeCreate) error
Delete(id uint) error
Delete(delete request.RuntimeDelete) error
Update(req request.RuntimeUpdate) error
Get(id uint) (res *response.RuntimeRes, err error)
Get(id uint) (res *response.RuntimeDTO, err error)
GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error)
}
func NewRuntimeService() IRuntimeService {
@ -37,24 +38,35 @@ func NewRuntimeService() IRuntimeService {
}
func (r *RuntimeService) Create(create request.RuntimeCreate) (err error) {
exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithName(create.Name))
exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithName(create.Name), commonRepo.WithByType(create.Type))
if exist != nil {
return buserr.New(constant.ErrNameIsExist)
}
if create.Resource == constant.ResourceLocal {
runtime := &model.Runtime{
Name: create.Name,
Resource: create.Resource,
Type: create.Type,
Version: create.Version,
Status: constant.RuntimeNormal,
fileOp := files.NewFileOp()
switch create.Type {
case constant.RuntimePHP:
if create.Resource == constant.ResourceLocal {
runtime := &model.Runtime{
Name: create.Name,
Resource: create.Resource,
Type: create.Type,
Version: create.Version,
Status: constant.RuntimeNormal,
}
return runtimeRepo.Create(context.Background(), runtime)
}
return runtimeRepo.Create(context.Background(), runtime)
}
exist, _ = runtimeRepo.GetFirst(runtimeRepo.WithImage(create.Image))
if exist != nil {
return buserr.New(constant.ErrImageExist)
exist, _ = runtimeRepo.GetFirst(runtimeRepo.WithImage(create.Image))
if exist != nil {
return buserr.New(constant.ErrImageExist)
}
case constant.RuntimeNode:
if !fileOp.Stat(create.CodeDir) {
return buserr.New(constant.ErrPathNotFound)
}
create.Install = true
}
appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(create.AppDetailID))
if err != nil {
return err
@ -63,64 +75,39 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (err error) {
if err != nil {
return err
}
fileOp := files.NewFileOp()
appVersionDir := path.Join(constant.AppResourceDir, app.Resource, app.Key, appDetail.Version)
if !fileOp.Stat(appVersionDir) || appDetail.Update {
if err := downloadApp(app, appDetail, nil); err != nil {
return err
}
}
buildDir := path.Join(appVersionDir, "build")
if !fileOp.Stat(buildDir) {
return buserr.New(constant.ErrDirNotFound)
}
runtimeDir := path.Join(constant.RuntimeDir, create.Type)
tempDir := filepath.Join(runtimeDir, fmt.Sprintf("%d", time.Now().UnixNano()))
if err = fileOp.CopyDir(buildDir, tempDir); err != nil {
return
}
oldDir := path.Join(tempDir, "build")
newNameDir := path.Join(runtimeDir, create.Name)
defer func() {
if err != nil {
_ = fileOp.DeleteDir(newNameDir)
}
}()
if oldDir != newNameDir {
if err = fileOp.Rename(oldDir, newNameDir); err != nil {
return
}
if err = fileOp.DeleteDir(tempDir); err != nil {
return
}
}
composeContent, envContent, forms, err := handleParams(create.Image, create.Type, newNameDir, create.Source, create.Params)
if err != nil {
return
}
runtime := &model.Runtime{
Name: create.Name,
DockerCompose: string(composeContent),
Env: string(envContent),
AppDetailID: create.AppDetailID,
Type: create.Type,
Image: create.Image,
Resource: create.Resource,
Status: constant.RuntimeBuildIng,
Version: create.Version,
Params: string(forms),
Name: create.Name,
AppDetailID: create.AppDetailID,
Type: create.Type,
Image: create.Image,
Resource: create.Resource,
Version: create.Version,
}
if err = runtimeRepo.Create(context.Background(), runtime); err != nil {
return
switch create.Type {
case constant.RuntimePHP:
if err = handlePHP(create, runtime, fileOp, appVersionDir); err != nil {
return
}
case constant.RuntimeNode:
if err = handleNode(create, runtime, fileOp, appVersionDir); err != nil {
return
}
}
go buildRuntime(runtime, "", false)
return
return runtimeRepo.Create(context.Background(), runtime)
}
func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.RuntimeRes, error) {
func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.RuntimeDTO, error) {
var (
opts []repo.DBOption
res []response.RuntimeRes
res []response.RuntimeDTO
)
if req.Name != "" {
opts = append(opts, commonRepo.WithLikeName(req.Name))
@ -128,116 +115,158 @@ func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.Runt
if req.Status != "" {
opts = append(opts, runtimeRepo.WithStatus(req.Status))
}
if req.Type != "" {
opts = append(opts, commonRepo.WithByType(req.Type))
}
total, runtimes, err := runtimeRepo.Page(req.Page, req.PageSize, opts...)
if err != nil {
return 0, nil, err
}
for _, runtime := range runtimes {
res = append(res, response.RuntimeRes{
Runtime: runtime,
})
runtimeDTO := response.NewRuntimeDTO(runtime)
runtimeDTO.Params = make(map[string]interface{})
envs, err := gotenv.Unmarshal(runtime.Env)
if err != nil {
return 0, nil, err
}
for k, v := range envs {
runtimeDTO.Params[k] = v
}
res = append(res, runtimeDTO)
}
return total, res, nil
}
func (r *RuntimeService) Delete(id uint) error {
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(id))
func (r *RuntimeService) Delete(runtimeDelete request.RuntimeDelete) error {
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(runtimeDelete.ID))
if err != nil {
return err
}
website, _ := websiteRepo.GetFirst(websiteRepo.WithRuntimeID(id))
website, _ := websiteRepo.GetFirst(websiteRepo.WithRuntimeID(runtimeDelete.ID))
if website.ID > 0 {
return buserr.New(constant.ErrDelWithWebsite)
}
if runtime.Resource == constant.ResourceAppstore {
client, err := docker.NewClient()
if err != nil {
return err
}
imageID, err := client.GetImageIDByName(runtime.Image)
if err != nil {
return err
}
if imageID != "" {
if err := client.DeleteImage(imageID); err != nil {
global.LOG.Errorf("delete image id [%s] error %v", imageID, err)
projectDir := runtime.GetPath()
switch runtime.Type {
case constant.RuntimePHP:
client, err := docker.NewClient()
if err != nil {
return err
}
imageID, err := client.GetImageIDByName(runtime.Image)
if err != nil {
return err
}
if imageID != "" {
if err := client.DeleteImage(imageID); err != nil {
global.LOG.Errorf("delete image id [%s] error %v", imageID, err)
}
}
case constant.RuntimeNode:
if out, err := compose.Down(runtime.GetComposePath()); err != nil && !runtimeDelete.ForceDelete {
if out != "" {
return errors.New(out)
}
return err
}
}
runtimeDir := path.Join(constant.RuntimeDir, runtime.Type, runtime.Name)
if err := files.NewFileOp().DeleteDir(runtimeDir); err != nil {
if err := files.NewFileOp().DeleteDir(projectDir); err != nil && !runtimeDelete.ForceDelete {
return err
}
}
return runtimeRepo.DeleteBy(commonRepo.WithByID(id))
return runtimeRepo.DeleteBy(commonRepo.WithByID(runtimeDelete.ID))
}
func (r *RuntimeService) Get(id uint) (*response.RuntimeRes, error) {
func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) {
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(id))
if err != nil {
return nil, err
}
res := &response.RuntimeRes{}
res.Runtime = *runtime
res := response.NewRuntimeDTO(*runtime)
if runtime.Resource == constant.ResourceLocal {
return res, nil
return &res, nil
}
appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(runtime.AppDetailID))
if err != nil {
return nil, err
}
res.AppID = appDetail.AppId
var (
appForm dto.AppForm
appParams []response.AppParam
)
if err := json.Unmarshal([]byte(runtime.Params), &appForm); err != nil {
return nil, err
}
envs, err := gotenv.Unmarshal(runtime.Env)
if err != nil {
return nil, err
}
if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok {
res.Source = v
}
for _, form := range appForm.FormFields {
if v, ok := envs[form.EnvKey]; ok {
appParam := response.AppParam{
Edit: false,
Key: form.EnvKey,
Rule: form.Rule,
Type: form.Type,
Required: form.Required,
}
if form.Edit {
appParam.Edit = true
}
appParam.LabelZh = form.LabelZh
appParam.LabelEn = form.LabelEn
appParam.Multiple = form.Multiple
appParam.Value = v
if form.Type == "select" {
if form.Multiple {
if v == "" {
appParam.Value = []string{}
switch runtime.Type {
case constant.RuntimePHP:
var (
appForm dto.AppForm
appParams []response.AppParam
)
if err := json.Unmarshal([]byte(runtime.Params), &appForm); err != nil {
return nil, err
}
envs, err := gotenv.Unmarshal(runtime.Env)
if err != nil {
return nil, err
}
if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok {
res.Source = v
}
for _, form := range appForm.FormFields {
if v, ok := envs[form.EnvKey]; ok {
appParam := response.AppParam{
Edit: false,
Key: form.EnvKey,
Rule: form.Rule,
Type: form.Type,
Required: form.Required,
}
if form.Edit {
appParam.Edit = true
}
appParam.LabelZh = form.LabelZh
appParam.LabelEn = form.LabelEn
appParam.Multiple = form.Multiple
appParam.Value = v
if form.Type == "select" {
if form.Multiple {
if v == "" {
appParam.Value = []string{}
} else {
appParam.Value = strings.Split(v, ",")
}
} else {
appParam.Value = strings.Split(v, ",")
}
} else {
for _, fv := range form.Values {
if fv.Value == v {
appParam.ShowValue = fv.Label
break
for _, fv := range form.Values {
if fv.Value == v {
appParam.ShowValue = fv.Label
break
}
}
}
appParam.Values = form.Values
}
appParam.Values = form.Values
appParams = append(appParams, appParam)
}
}
res.AppParams = appParams
case constant.RuntimeNode:
res.Params = make(map[string]interface{})
envs, err := gotenv.Unmarshal(runtime.Env)
if err != nil {
return nil, err
}
for k, v := range envs {
switch k {
case "NODE_APP_PORT", "PANEL_APP_PORT_HTTP":
port, err := strconv.Atoi(v)
if err != nil {
return nil, err
}
res.Params[k] = port
default:
res.Params[k] = v
}
appParams = append(appParams, appParam)
}
}
res.AppParams = appParams
return res, nil
return &res, nil
}
func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
@ -245,36 +274,86 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
if err != nil {
return err
}
oldImage := runtime.Image
if runtime.Resource == constant.ResourceLocal {
runtime.Version = req.Version
return runtimeRepo.Save(runtime)
}
exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithImage(req.Name), runtimeRepo.WithNotId(req.ID))
if exist != nil {
return buserr.New(constant.ErrImageExist)
oldImage := runtime.Image
switch runtime.Type {
case constant.RuntimePHP:
exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithImage(req.Name), runtimeRepo.WithNotId(req.ID))
if exist != nil {
return buserr.New(constant.ErrImageExist)
}
}
runtimeDir := path.Join(constant.RuntimeDir, runtime.Type, runtime.Name)
composeContent, envContent, _, err := handleParams(req.Image, runtime.Type, runtimeDir, req.Source, req.Params)
projectDir := path.Join(constant.RuntimeDir, runtime.Type, runtime.Name)
create := request.RuntimeCreate{
Image: req.Image,
Type: runtime.Type,
Source: req.Source,
Params: req.Params,
CodeDir: req.CodeDir,
Version: req.Version,
}
composeContent, envContent, _, err := handleParams(create, projectDir)
if err != nil {
return err
}
if err != nil {
return err
}
runtime.Image = req.Image
runtime.Env = string(envContent)
runtime.DockerCompose = string(composeContent)
runtime.Status = constant.RuntimeBuildIng
_ = runtimeRepo.Save(runtime)
client, err := docker.NewClient()
if err != nil {
return err
switch runtime.Type {
case constant.RuntimePHP:
runtime.Image = req.Image
runtime.Status = constant.RuntimeBuildIng
_ = runtimeRepo.Save(runtime)
client, err := docker.NewClient()
if err != nil {
return err
}
imageID, err := client.GetImageIDByName(oldImage)
if err != nil {
return err
}
go buildRuntime(runtime, imageID, req.Rebuild)
case constant.RuntimeNode:
runtime.Version = req.Version
runtime.CodeDir = req.CodeDir
runtime.Status = constant.RuntimeReCreating
_ = runtimeRepo.Save(runtime)
go reCreateRuntime(runtime)
}
imageID, err := client.GetImageIDByName(oldImage)
if err != nil {
return err
}
go buildRuntime(runtime, imageID, req.Rebuild)
return nil
}
func (r *RuntimeService) GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error) {
fileOp := files.NewFileOp()
if !fileOp.Stat(req.CodeDir) {
return nil, buserr.New(constant.ErrPathNotFound)
}
if !fileOp.Stat(path.Join(req.CodeDir, "package.json")) {
return nil, buserr.New(constant.ErrPackageJsonNotFound)
}
content, err := fileOp.GetContent(path.Join(req.CodeDir, "package.json"))
if err != nil {
return nil, err
}
var packageMap map[string]interface{}
err = json.Unmarshal(content, &packageMap)
if err != nil {
return nil, err
}
scripts, ok := packageMap["scripts"]
if !ok {
return nil, buserr.New(constant.ErrScriptsNotFound)
}
var packageScripts []response.PackageScripts
for k, v := range scripts.(map[string]interface{}) {
packageScripts = append(packageScripts, response.PackageScripts{
Name: k,
Script: v.(string),
})
}
return packageScripts, nil
}

View File

@ -10,22 +10,159 @@ import (
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/pkg/errors"
"github.com/subosito/gotenv"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"time"
)
func handleNode(create request.RuntimeCreate, runtime *model.Runtime, fileOp files.FileOp, appVersionDir string) (err error) {
runtimeDir := path.Join(constant.RuntimeDir, create.Type)
if err = fileOp.CopyDir(appVersionDir, runtimeDir); err != nil {
return
}
versionDir := path.Join(runtimeDir, filepath.Base(appVersionDir))
projectDir := path.Join(runtimeDir, create.Name)
defer func() {
if err != nil {
_ = fileOp.DeleteDir(projectDir)
}
}()
if err = fileOp.Rename(versionDir, projectDir); err != nil {
return
}
composeContent, envContent, _, err := handleParams(create, projectDir)
if err != nil {
return
}
runtime.DockerCompose = string(composeContent)
runtime.Env = string(envContent)
runtime.Status = constant.RuntimeStarting
runtime.CodeDir = create.CodeDir
go startRuntime(runtime)
return
}
func handlePHP(create request.RuntimeCreate, runtime *model.Runtime, fileOp files.FileOp, appVersionDir string) (err error) {
buildDir := path.Join(appVersionDir, "build")
if !fileOp.Stat(buildDir) {
return buserr.New(constant.ErrDirNotFound)
}
runtimeDir := path.Join(constant.RuntimeDir, create.Type)
tempDir := filepath.Join(runtimeDir, fmt.Sprintf("%d", time.Now().UnixNano()))
if err = fileOp.CopyDir(buildDir, tempDir); err != nil {
return
}
oldDir := path.Join(tempDir, "build")
projectDir := path.Join(runtimeDir, create.Name)
defer func() {
if err != nil {
_ = fileOp.DeleteDir(projectDir)
}
}()
if oldDir != projectDir {
if err = fileOp.Rename(oldDir, projectDir); err != nil {
return
}
if err = fileOp.DeleteDir(tempDir); err != nil {
return
}
}
composeContent, envContent, forms, err := handleParams(create, projectDir)
if err != nil {
return
}
runtime.DockerCompose = string(composeContent)
runtime.Env = string(envContent)
runtime.Params = string(forms)
runtime.Status = constant.RuntimeBuildIng
go buildRuntime(runtime, "", false)
return
}
func startRuntime(runtime *model.Runtime) {
cmd := exec.Command("docker-compose", "-f", runtime.GetComposePath(), "up", "-d")
logPath := runtime.GetLogPath()
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
global.LOG.Errorf("Failed to open log file: %v", err)
return
}
multiWriterStdout := io.MultiWriter(os.Stdout, logFile)
cmd.Stdout = multiWriterStdout
var stderrBuf bytes.Buffer
multiWriterStderr := io.MultiWriter(&stderrBuf, logFile, os.Stderr)
cmd.Stderr = multiWriterStderr
err = cmd.Run()
if err != nil {
runtime.Status = constant.RuntimeError
runtime.Message = buserr.New(constant.ErrRuntimeStart).Error() + ":" + stderrBuf.String()
} else {
runtime.Status = constant.RuntimeRunning
runtime.Message = ""
}
_ = runtimeRepo.Save(runtime)
}
func runComposeCmdWithLog(operate string, composePath string, logPath string) error {
cmd := exec.Command("docker-compose", "-f", composePath, operate)
if operate == "up" {
cmd = exec.Command("docker-compose", "-f", composePath, operate, "-d")
}
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
global.LOG.Errorf("Failed to open log file: %v", err)
return err
}
multiWriterStdout := io.MultiWriter(os.Stdout, logFile)
cmd.Stdout = multiWriterStdout
var stderrBuf bytes.Buffer
multiWriterStderr := io.MultiWriter(&stderrBuf, logFile, os.Stderr)
cmd.Stderr = multiWriterStderr
err = cmd.Run()
if err != nil {
return errors.New(buserr.New(constant.ErrRuntimeStart).Error() + ":" + stderrBuf.String())
}
return nil
}
func reCreateRuntime(runtime *model.Runtime) {
var err error
defer func() {
if err != nil {
runtime.Status = constant.RuntimeError
runtime.Message = err.Error()
_ = runtimeRepo.Save(runtime)
}
}()
if err = runComposeCmdWithLog("down", runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
return
}
if err = runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
return
}
runtime.Status = constant.RuntimeRunning
_ = runtimeRepo.Save(runtime)
}
func buildRuntime(runtime *model.Runtime, oldImageID string, rebuild bool) {
runtimePath := path.Join(constant.RuntimeDir, runtime.Type, runtime.Name)
composePath := path.Join(runtimePath, "docker-compose.yml")
runtimePath := runtime.GetPath()
composePath := runtime.GetComposePath()
logPath := path.Join(runtimePath, "build.log")
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
fmt.Println("Failed to open log file:", err)
global.LOG.Errorf("failed to open log file: %v", err)
return
}
defer func() {
@ -89,35 +226,45 @@ func buildRuntime(runtime *model.Runtime, oldImageID string, rebuild bool) {
_ = runtimeRepo.Save(runtime)
}
func handleParams(image, runtimeType, runtimeDir, source string, params map[string]interface{}) (composeContent []byte, envContent []byte, forms []byte, err error) {
func handleParams(create request.RuntimeCreate, projectDir string) (composeContent []byte, envContent []byte, forms []byte, err error) {
fileOp := files.NewFileOp()
composeContent, err = fileOp.GetContent(path.Join(runtimeDir, "docker-compose.yml"))
composeContent, err = fileOp.GetContent(path.Join(projectDir, "docker-compose.yml"))
if err != nil {
return
}
env, err := gotenv.Read(path.Join(runtimeDir, ".env"))
env, err := gotenv.Read(path.Join(projectDir, ".env"))
if err != nil {
return
}
forms, err = fileOp.GetContent(path.Join(runtimeDir, "config.json"))
if err != nil {
return
}
params["IMAGE_NAME"] = image
if runtimeType == constant.RuntimePHP {
if extends, ok := params["PHP_EXTENSIONS"]; ok {
switch create.Type {
case constant.RuntimePHP:
create.Params["IMAGE_NAME"] = create.Image
forms, err = fileOp.GetContent(path.Join(projectDir, "config.json"))
if err != nil {
return
}
if extends, ok := create.Params["PHP_EXTENSIONS"]; ok {
if extendsArray, ok := extends.([]interface{}); ok {
strArray := make([]string, len(extendsArray))
for i, v := range extendsArray {
strArray[i] = strings.ToLower(fmt.Sprintf("%v", v))
}
params["PHP_EXTENSIONS"] = strings.Join(strArray, ",")
create.Params["PHP_EXTENSIONS"] = strings.Join(strArray, ",")
}
}
params["CONTAINER_PACKAGE_URL"] = source
create.Params["CONTAINER_PACKAGE_URL"] = create.Source
case constant.RuntimeNode:
create.Params["CODE_DIR"] = create.CodeDir
create.Params["NODE_VERSION"] = create.Version
if create.NodeConfig.Install {
create.Params["RUN_INSTALL"] = "1"
} else {
create.Params["RUN_INSTALL"] = "0"
}
}
newMap := make(map[string]string)
handleMap(params, newMap)
handleMap(create.Params, newMap)
for k, v := range newMap {
env[k] = v
}
@ -125,7 +272,7 @@ func handleParams(image, runtimeType, runtimeDir, source string, params map[stri
if err != nil {
return
}
if err = gotenv.Write(env, path.Join(runtimeDir, ".env")); err != nil {
if err = gotenv.Write(env, path.Join(projectDir, ".env")); err != nil {
return
}
envContent = []byte(envStr)

View File

@ -114,11 +114,14 @@ var (
// runtime
var (
ErrDirNotFound = "ErrDirNotFound"
ErrFileNotExist = "ErrFileNotExist"
ErrImageBuildErr = "ErrImageBuildErr"
ErrImageExist = "ErrImageExist"
ErrDelWithWebsite = "ErrDelWithWebsite"
ErrDirNotFound = "ErrDirNotFound"
ErrFileNotExist = "ErrFileNotExist"
ErrImageBuildErr = "ErrImageBuildErr"
ErrImageExist = "ErrImageExist"
ErrDelWithWebsite = "ErrDelWithWebsite"
ErrRuntimeStart = "ErrRuntimeStart"
ErrPackageJsonNotFound = "ErrPackageJsonNotFound"
ErrScriptsNotFound = "ErrScriptsNotFound"
)
var (

View File

@ -4,11 +4,15 @@ const (
ResourceLocal = "local"
ResourceAppstore = "appstore"
RuntimeNormal = "normal"
RuntimeError = "error"
RuntimeBuildIng = "building"
RuntimeNormal = "normal"
RuntimeError = "error"
RuntimeBuildIng = "building"
RuntimeStarting = "starting"
RuntimeRunning = "running"
RuntimeReCreating = "recreating"
RuntimePHP = "php"
RuntimePHP = "php"
RuntimeNode = "node"
RuntimeProxyUnix = "unix"
RuntimeProxyTcp = "tcp"

View File

@ -99,6 +99,9 @@ ErrFileNotExist: "{{ .detail }} file does not exist! Please check source file in
ErrImageBuildErr: "Image build failed"
ErrImageExist: "Image is already exist"
ErrDelWithWebsite: "The operating environment has been associated with a website and cannot be deleted"
ErrRuntimeStart: "Failed to start"
ErrPackageJsonNotFound: "package.json file does not exist"
ErrScriptsNotFound: "No scripts configuration item was found in package.json"
#setting
ErrBackupInUsed: "The backup account is already being used in a cronjob and cannot be deleted."

View File

@ -99,6 +99,9 @@ ErrFileNotExist: "{{ .detail }} 文件不存在!請檢查源文件完整性!
ErrImageBuildErr: "鏡像 build 失敗"
ErrImageExist: "鏡像已存在!"
ErrDelWithWebsite: "運行環境已經關聯網站,無法刪除"
ErrRuntimeStart: "啟動失敗"
ErrPackageJsonNotFound: "package.json 文件不存在"
ErrScriptsNotFound: "沒有在 package.json 中找到 scripts 配置項"
#setting
ErrBackupInUsed: "該備份帳號已在計劃任務中使用,無法刪除"

View File

@ -99,6 +99,9 @@ ErrFileNotExist: "{{ .detail }} 文件不存在!请检查源文件完整性!
ErrImageBuildErr: "镜像 build 失败"
ErrImageExist: "镜像已存在!"
ErrDelWithWebsite: "运行环境已经关联网站,无法删除"
ErrRuntimeStart: "启动失败"
ErrPackageJsonNotFound: "package.json 文件不存在"
ErrScriptsNotFound: "没有在 package.json 中找到 scripts 配置项"
#setting
ErrBackupInUsed: "该备份账号已在计划任务中使用,无法删除"

View File

@ -45,6 +45,7 @@ func Init() {
migrations.DropDatabaseLocal,
migrations.AddDefaultNetwork,
migrations.UpdateRuntime,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View File

@ -15,3 +15,13 @@ var AddDefaultNetwork = &gormigrate.Migration{
return nil
},
}
var UpdateRuntime = &gormigrate.Migration{
ID: "20230920-update-runtime",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Runtime{}); err != nil {
return err
}
return nil
},
}

View File

@ -20,5 +20,6 @@ func (r *RuntimeRouter) InitRuntimeRouter(Router *gin.RouterGroup) {
groupRouter.POST("/del", baseApi.DeleteRuntime)
groupRouter.POST("/update", baseApi.UpdateRuntime)
groupRouter.GET("/:id", baseApi.GetRuntime)
groupRouter.POST("/node/package", baseApi.GetNodePackageRunScript)
}
}

View File

@ -1,5 +1,5 @@
// Code generated by swaggo/swag. DO NOT EDIT.
// Package docs GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag
package docs
import "github.com/swaggo/swag"
@ -7830,6 +7830,39 @@ const docTemplate = `{
}
}
},
"/runtimes/node/package": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Node 项目的 scripts",
"consumes": [
"application/json"
],
"tags": [
"Runtime"
],
"summary": "Get Node package scripts",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.NodePackageReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/runtimes/search": {
"post": {
"security": [
@ -12262,8 +12295,26 @@ const docTemplate = `{
"cpuPercent": {
"type": "number"
},
"cpuTotalUsage": {
"type": "integer"
},
"memoryCache": {
"type": "integer"
},
"memoryLimit": {
"type": "integer"
},
"memoryPercent": {
"type": "number"
},
"memoryUsage": {
"type": "integer"
},
"percpuUsage": {
"type": "integer"
},
"systemUsage": {
"type": "integer"
}
}
},
@ -16357,6 +16408,14 @@ const docTemplate = `{
}
}
},
"request.NodePackageReq": {
"type": "object",
"properties": {
"codeDir": {
"type": "string"
}
}
},
"request.PortUpdate": {
"type": "object",
"properties": {
@ -16388,9 +16447,18 @@ const docTemplate = `{
"appDetailId": {
"type": "integer"
},
"clean": {
"type": "boolean"
},
"codeDir": {
"type": "string"
},
"image": {
"type": "string"
},
"install": {
"type": "boolean"
},
"name": {
"type": "string"
},
@ -16415,6 +16483,9 @@ const docTemplate = `{
"request.RuntimeDelete": {
"type": "object",
"properties": {
"forceDelete": {
"type": "boolean"
},
"id": {
"type": "integer"
}
@ -16447,12 +16518,21 @@ const docTemplate = `{
"request.RuntimeUpdate": {
"type": "object",
"properties": {
"clean": {
"type": "boolean"
},
"codeDir": {
"type": "string"
},
"id": {
"type": "integer"
},
"image": {
"type": "string"
},
"install": {
"type": "boolean"
},
"name": {
"type": "string"
},

View File

@ -7823,6 +7823,39 @@
}
}
},
"/runtimes/node/package": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Node 项目的 scripts",
"consumes": [
"application/json"
],
"tags": [
"Runtime"
],
"summary": "Get Node package scripts",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.NodePackageReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/runtimes/search": {
"post": {
"security": [
@ -12255,8 +12288,26 @@
"cpuPercent": {
"type": "number"
},
"cpuTotalUsage": {
"type": "integer"
},
"memoryCache": {
"type": "integer"
},
"memoryLimit": {
"type": "integer"
},
"memoryPercent": {
"type": "number"
},
"memoryUsage": {
"type": "integer"
},
"percpuUsage": {
"type": "integer"
},
"systemUsage": {
"type": "integer"
}
}
},
@ -16350,6 +16401,14 @@
}
}
},
"request.NodePackageReq": {
"type": "object",
"properties": {
"codeDir": {
"type": "string"
}
}
},
"request.PortUpdate": {
"type": "object",
"properties": {
@ -16381,9 +16440,18 @@
"appDetailId": {
"type": "integer"
},
"clean": {
"type": "boolean"
},
"codeDir": {
"type": "string"
},
"image": {
"type": "string"
},
"install": {
"type": "boolean"
},
"name": {
"type": "string"
},
@ -16408,6 +16476,9 @@
"request.RuntimeDelete": {
"type": "object",
"properties": {
"forceDelete": {
"type": "boolean"
},
"id": {
"type": "integer"
}
@ -16440,12 +16511,21 @@
"request.RuntimeUpdate": {
"type": "object",
"properties": {
"clean": {
"type": "boolean"
},
"codeDir": {
"type": "string"
},
"id": {
"type": "integer"
},
"image": {
"type": "string"
},
"install": {
"type": "boolean"
},
"name": {
"type": "string"
},

View File

@ -339,8 +339,20 @@ definitions:
type: string
cpuPercent:
type: number
cpuTotalUsage:
type: integer
memoryCache:
type: integer
memoryLimit:
type: integer
memoryPercent:
type: number
memoryUsage:
type: integer
percpuUsage:
type: integer
systemUsage:
type: integer
type: object
dto.ContainerOperate:
properties:
@ -3085,6 +3097,11 @@ definitions:
required:
- scope
type: object
request.NodePackageReq:
properties:
codeDir:
type: string
type: object
request.PortUpdate:
properties:
key:
@ -3105,8 +3122,14 @@ definitions:
properties:
appDetailId:
type: integer
clean:
type: boolean
codeDir:
type: string
image:
type: string
install:
type: boolean
name:
type: string
params:
@ -3123,6 +3146,8 @@ definitions:
type: object
request.RuntimeDelete:
properties:
forceDelete:
type: boolean
id:
type: integer
type: object
@ -3144,10 +3169,16 @@ definitions:
type: object
request.RuntimeUpdate:
properties:
clean:
type: boolean
codeDir:
type: string
id:
type: integer
image:
type: string
install:
type: boolean
name:
type: string
params:
@ -9035,6 +9066,26 @@ paths:
formatEN: Delete website [name]
formatZH: 删除网站 [name]
paramKeys: []
/runtimes/node/package:
post:
consumes:
- application/json
description: 获取 Node 项目的 scripts
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.NodePackageReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Get Node package scripts
tags:
- Runtime
/runtimes/search:
post:
consumes:

View File

@ -3,7 +3,7 @@ import { App } from './app';
export namespace Runtime {
export interface Runtime extends CommonModel {
name: string;
appDetailId: number;
appDetailID: number;
image: string;
workDir: string;
dockerCompose: string;
@ -13,46 +13,59 @@ export namespace Runtime {
resource: string;
version: string;
status: string;
codeDir: string;
}
export interface RuntimeReq extends ReqPage {
name?: string;
status?: string;
type?: string;
}
export interface NodeReq {
codeDir: string;
}
export interface NodeScripts {
name: string;
script: string;
}
export interface RuntimeDTO extends Runtime {
appParams: App.InstallParams[];
appId: number;
appID: number;
source?: string;
}
export interface RuntimeCreate {
id?: number;
name: string;
appDetailId: number;
appDetailID: number;
image: string;
params: object;
type: string;
resource: string;
appId?: number;
appID?: number;
version?: string;
rebuild?: boolean;
source?: string;
codeDir?: string;
}
export interface RuntimeUpdate {
name: string;
appDetailId: number;
appDetailID: number;
image: string;
params: object;
type: string;
resource: string;
appId?: number;
appID?: number;
version?: string;
rebuild?: boolean;
}
export interface RuntimeDelete {
id: number;
forceDelete: boolean;
}
}

View File

@ -21,3 +21,7 @@ export const GetRuntime = (id: number) => {
export const UpdateRuntime = (req: Runtime.RuntimeUpdate) => {
return http.post<any>(`/runtimes/update`, req);
};
export const GetNodeScripts = (req: Runtime.NodeReq) => {
return http.post<Runtime.NodeScripts[]>(`/runtimes/node/package`, req);
};

View File

@ -8,7 +8,7 @@
popper-class="file-list"
>
<template #reference>
<el-button :icon="Folder" @click="popoverVisible = true"></el-button>
<el-button :icon="Folder" :disabled="disabled" @click="popoverVisible = true"></el-button>
</template>
<div>
<el-button class="close" link @click="closePage">
@ -116,6 +116,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
});
const em = defineEmits(['choose']);

View File

@ -34,7 +34,7 @@ const getType = (status: string) => {
}
};
const loadingStatus = ['installing', 'building', 'restarting', 'upgrading', 'rebuilding'];
const loadingStatus = ['installing', 'building', 'restarting', 'upgrading', 'rebuilding', 'recreating', 'creating'];
const loadingIcon = (status: string): boolean => {
return loadingStatus.indexOf(status) > -1;

View File

@ -228,6 +228,8 @@ const message = {
accept: 'Accepted',
used: 'Used',
unUsed: 'Unused',
starting: 'Starting',
recreating: 'Recreating',
},
units: {
second: 'Second',
@ -1691,6 +1693,16 @@ const message = {
xtomhk: 'XTOM Mirror Station (Hong Kong)',
xtom: 'XTOM Mirror Station (Global)',
phpsourceHelper: 'Choose the appropriate source according to your network environment',
appPort: 'App Port',
externalPort: 'External Port',
packageManager: 'Package Manager',
codeDir: 'Code Directory',
appPortHelper: 'The port used by the application',
externalPortHelper: 'The port exposed to the outside world',
runScript: 'Run Script',
runScriptHelper: 'The startup command list is parsed from the package.json file in the source directory',
open: 'Open',
close: 'Close',
},
process: {
pid: 'Process ID',

View File

@ -226,6 +226,8 @@ const message = {
accept: '已放行',
used: '已使用',
unUsed: '未使用',
starting: '啟動中',
recreating: '重建中',
},
units: {
second: '秒',
@ -1600,6 +1602,16 @@ const message = {
xtomhk: 'XTOM 鏡像站香港',
xtom: 'XTOM 鏡像站全球',
phpsourceHelper: '根據你的網絡環境選擇合適的源',
appPort: '應用端口',
externalPort: '外部映射端口',
packageManager: '包管理器',
codeDir: '源碼目錄',
appPortHelper: '應用端口是指容器內部運行的端口',
externalPortHelper: '外部映射端口是指將容器內部端口映射到外部的端口',
runScript: '啟動命令',
runScriptHelper: '啟動命令是指容器啟動後運行的命令',
open: '開啟',
close: '關閉',
},
process: {
pid: '進程ID',

View File

@ -226,6 +226,8 @@ const message = {
accept: '已放行',
used: '已使用',
unUsed: '未使用',
starting: '启动中',
recreating: '重建中',
},
units: {
second: '秒',
@ -1600,6 +1602,16 @@ const message = {
xtomhk: 'XTOM 镜像站香港',
xtom: 'XTOM 镜像站全球',
phpsourceHelper: '根据你的网络环境选择合适的源',
appPort: '应用端口',
externalPort: '外部映射端口',
packageManager: '包管理器',
codeDir: '源码目录',
appPortHelper: '应用端口是指容器内部的端口',
externalPortHelper: '外部映射端口是指容器对外暴露的端口',
runScript: '启动命令',
runScriptHelper: '启动命令列表是从源码目录下的 package.json 文件中解析而来',
open: '放开',
close: '关闭',
},
process: {
pid: '进程ID',

View File

@ -40,14 +40,24 @@ const webSiteRouter = {
},
},
{
path: '/websites/runtime/php',
name: 'Runtime',
component: () => import('@/views/website/runtime/index.vue'),
path: '/websites/runtimes/php',
name: 'PHP',
component: () => import('@/views/website/runtime/php/index.vue'),
meta: {
title: 'menu.runtime',
requiresAuth: false,
},
},
{
path: '/websites/runtimes/node',
name: 'Node',
hidden: true,
component: () => import('@/views/website/runtime/node/index.vue'),
meta: {
activeMenu: '/websites/runtimes/php',
requiresAuth: false,
},
},
],
};

View File

@ -203,13 +203,18 @@ const search = async (req: App.AppReq) => {
};
const openInstall = (app: App.App) => {
if (app.type === 'php') {
router.push({ path: '/websites/runtime/php' });
} else {
const params = {
app: app,
};
installRef.value.acceptParams(params);
switch (app.type) {
case 'php':
router.push({ path: '/websites/runtimes/php' });
break;
case 'node':
router.push({ path: '/websites/runtimes/node' });
break;
default:
const params = {
app: app,
};
installRef.value.acceptParams(params);
}
};

View File

@ -0,0 +1,77 @@
<template>
<el-dialog
v-model="open"
:close-on-click-modal="false"
:title="$t('commons.button.delete') + ' - ' + resourceName"
width="30%"
:before-close="handleClose"
>
<div :key="key" :loading="loading">
<el-form ref="deleteForm" label-position="left">
<el-form-item>
<el-checkbox v-model="deleteReq.forceDelete" :label="$t('website.forceDelete')" />
<span class="input-help">
{{ $t('website.forceDeleteHelper') }}
</span>
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit()" :loading="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { DeleteRuntime } from '@/api/modules/runtime';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { ref } from 'vue';
const key = 1;
const open = ref(false);
const loading = ref(false);
const deleteReq = ref({
id: 0,
forceDelete: false,
});
const em = defineEmits(['close']);
const deleteForm = ref<FormInstance>();
const resourceName = ref('');
const handleClose = () => {
open.value = false;
em('close', false);
};
const acceptParams = async (id: number, name: string) => {
deleteReq.value = {
id: id,
forceDelete: false,
};
resourceName.value = name;
open.value = true;
};
const submit = () => {
loading.value = true;
DeleteRuntime(deleteReq.value)
.then(() => {
handleClose();
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
})
.finally(() => {
loading.value = false;
});
};
defineExpose({
acceptParams,
});
</script>

View File

@ -1,161 +1,21 @@
<template>
<div>
<RouterButton
:buttons="[
{
label: 'PHP',
path: '/runtimes/php',
},
]"
/>
<LayoutContent :title="$t('runtime.runtime')" v-loading="loading">
<template #toolbar>
<el-button type="primary" @click="openCreate">
{{ $t('runtime.create') }}
</el-button>
</template>
<template #main>
<ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()">
<el-table-column :label="$t('commons.table.name')" fix prop="name" min-width="120px">
<template #default="{ row }">
<Tooltip :text="row.name" @click="openDetail(row)" />
</template>
</el-table-column>
<el-table-column :label="$t('runtime.resource')" prop="resource">
<template #default="{ row }">
<span>{{ $t('runtime.' + toLowerCase(row.resource)) }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('runtime.version')" prop="version"></el-table-column>
<el-table-column :label="$t('runtime.image')" prop="image" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('commons.table.status')" prop="status">
<template #default="{ row }">
<el-popover
v-if="row.status === 'error'"
placement="bottom"
:width="400"
trigger="hover"
:content="row.message"
>
<template #reference>
<Status :key="row.status" :status="row.status"></Status>
</template>
</el-popover>
<div v-else>
<Status :key="row.status" :status="row.status"></Status>
</div>
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFormat"
show-overflow-tooltip
min-width="120"
fix
/>
<fu-table-operations
:ellipsis="10"
width="120px"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
</template>
<RouterButton :buttons="buttons" />
<LayoutContent>
<router-view></router-view>
</LayoutContent>
<CreateRuntime ref="createRef" @close="search" />
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { Runtime } from '@/api/interface/runtime';
import { DeleteRuntime, SearchRuntimes } from '@/api/modules/runtime';
import { dateFormat, toLowerCase } from '@/utils/util';
import CreateRuntime from '@/views/website/runtime/create/index.vue';
import Status from '@/components/status/index.vue';
import i18n from '@/lang';
import { useDeleteData } from '@/hooks/use-delete-data';
const paginationConfig = reactive({
cacheSizeKey: 'runtime-page-size',
currentPage: 1,
pageSize: 10,
total: 0,
});
let req = reactive<Runtime.RuntimeReq>({
name: '',
page: 1,
pageSize: 40,
});
let timer: NodeJS.Timer | null = null;
<script lang="ts" setup>
const buttons = [
{
label: i18n.global.t('commons.button.edit'),
click: function (row: Runtime.Runtime) {
openDetail(row);
},
disabled: function (row: Runtime.Runtime) {
return row.status === 'building';
},
label: 'PHP',
path: '/websites/runtimes/php',
},
{
label: i18n.global.t('commons.button.delete'),
click: function (row: Runtime.Runtime) {
openDelete(row);
},
label: 'Node.js',
path: '/websites/runtimes/node',
},
];
const loading = ref(false);
const items = ref<Runtime.RuntimeDTO[]>([]);
const createRef = ref();
const search = async () => {
req.page = paginationConfig.currentPage;
req.pageSize = paginationConfig.pageSize;
loading.value = true;
try {
const res = await SearchRuntimes(req);
items.value = res.data.items;
paginationConfig.total = res.data.total;
} catch (error) {
} finally {
loading.value = false;
}
};
const openCreate = () => {
createRef.value.acceptParams({ type: 'php', mode: 'create' });
};
const openDetail = (row: Runtime.Runtime) => {
createRef.value.acceptParams({ type: row.type, mode: 'edit', id: row.id });
};
const openDelete = async (row: Runtime.Runtime) => {
await useDeleteData(DeleteRuntime, { id: row.id }, 'commons.msg.delete');
search();
};
onMounted(() => {
search();
timer = setInterval(() => {
search();
}, 10000 * 3);
});
onUnmounted(() => {
clearInterval(Number(timer));
timer = null;
});
</script>
<style lang="scss" scoped>
.open-warn {
color: $primary-color;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,337 @@
<template>
<el-drawer :close-on-click-modal="false" v-model="open" size="50%">
<template #header>
<DrawerHeader :header="$t('runtime.' + mode)" :resource="runtime.name" :back="handleClose" />
</template>
<el-row v-loading="loading">
<el-col :span="22" :offset="1">
<el-form
ref="runtimeForm"
label-position="top"
:model="runtime"
label-width="125px"
:rules="rules"
:validate-on-rule-change="false"
>
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input :disabled="mode === 'edit'" v-model="runtime.name"></el-input>
</el-form-item>
<el-form-item :label="$t('runtime.app')" prop="appId">
<el-row :gutter="20">
<el-col :span="12">
<el-select
v-model="runtime.appId"
:disabled="mode === 'edit'"
@change="changeApp(runtime.appId)"
>
<el-option
v-for="(app, index) in apps"
:key="index"
:label="app.name"
:value="app.id"
></el-option>
</el-select>
</el-col>
<el-col :span="12">
<el-select
v-model="runtime.version"
:disabled="mode === 'edit'"
@change="changeVersion()"
>
<el-option
v-for="(version, index) in appVersions"
:key="index"
:label="version"
:value="version"
></el-option>
</el-select>
</el-col>
</el-row>
</el-form-item>
<el-form-item :label="$t('runtime.codeDir')" prop="codeDir">
<el-input v-model.trim="runtime.codeDir">
<template #prepend>
<FileList :path="runtime.codeDir" @choose="getPath" :dir="true"></FileList>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('runtime.runScript')" prop="params.EXEC_SCRIPT">
<el-select v-model="runtime.params['EXEC_SCRIPT']">
<el-option
v-for="(script, index) in scripts"
:key="index"
:label="script.name + ' 【 ' + script.script + ' 】'"
:value="script.name"
>
<el-row :gutter="10">
<el-col :span="4">{{ script.name }}</el-col>
<el-col :span="10">{{ ' 【 ' + script.script + ' 】' }}</el-col>
</el-row>
</el-option>
</el-select>
</el-form-item>
<el-row :gutter="20">
<el-col :span="10">
<el-form-item :label="$t('runtime.appPort')" prop="params.NODE_APP_PORT">
<el-input v-model.number="runtime.params['NODE_APP_PORT']" />
<span class="input-help">{{ $t('runtime.appPortHelper') }}</span>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item :label="$t('runtime.externalPort')" prop="params.PANEL_APP_PORT_HTTP">
<el-input v-model.number="runtime.params['PANEL_APP_PORT_HTTP']" />
<span class="input-help">{{ $t('runtime.externalPortHelper') }}</span>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item :label="$t('app.allowPort')" prop="params.HOST_IP">
<el-select v-model="runtime.params['HOST_IP']">
<el-option label="放开" value="0.0.0.0"></el-option>
<el-option label="不放开" value="127.0.0.1"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="$t('runtime.packageManager')" prop="params.PACKAGE_MANAGER">
<el-select v-model="runtime.params['PACKAGE_MANAGER']">
<el-option label="npm" value="npm"></el-option>
<el-option label="yarn" value="yarn"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('app.containerName')" prop="params.CONTAINER_NAME">
<el-input v-model.trim="runtime.params['CONTAINER_NAME']"></el-input>
</el-form-item>
</el-form>
</el-col>
</el-row>
<template #footer>
<span>
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(runtimeForm)" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { App } from '@/api/interface/app';
import { Runtime } from '@/api/interface/runtime';
import { GetApp, GetAppDetail, SearchApp } from '@/api/modules/app';
import { CreateRuntime, GetNodeScripts, GetRuntime, UpdateRuntime } from '@/api/modules/runtime';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { reactive, ref, watch } from 'vue';
import DrawerHeader from '@/components/drawer-header/index.vue';
interface OperateRrops {
id?: number;
mode: string;
type: string;
}
const open = ref(false);
const apps = ref<App.App[]>([]);
const runtimeForm = ref<FormInstance>();
const loading = ref(false);
const mode = ref('create');
const editParams = ref<App.InstallParams[]>();
const appVersions = ref<string[]>([]);
const appReq = reactive({
type: 'node',
page: 1,
pageSize: 20,
});
const initData = (type: string) => ({
name: '',
appDetailId: undefined,
image: '',
params: {
PACKAGE_MANAGER: 'npm',
HOST_IP: '0.0.0.0',
},
type: type,
resource: 'appstore',
rebuild: false,
codeDir: '/',
});
let runtime = reactive<Runtime.RuntimeCreate>(initData('node'));
const rules = ref<any>({
name: [Rules.appName],
appId: [Rules.requiredSelect],
codeDir: [Rules.requiredInput],
params: {
NODE_APP_PORT: [Rules.requiredInput, Rules.port],
PANEL_APP_PORT_HTTP: [Rules.requiredInput, Rules.port],
PACKAGE_MANAGER: [Rules.requiredSelect],
HOST_IP: [Rules.requiredSelect],
EXEC_SCRIPT: [Rules.requiredSelect],
CONTAINER_NAME: [Rules.requiredInput],
},
});
const scripts = ref<Runtime.NodeScripts[]>([]);
const em = defineEmits(['close']);
watch(
() => runtime.params['NODE_APP_PORT'],
(newVal) => {
if (newVal) {
runtime.params['PANEL_APP_PORT_HTTP'] = newVal;
}
},
{ deep: true },
);
watch(
() => runtime.name,
(newVal) => {
if (newVal) {
runtime.params['CONTAINER_NAME'] = newVal;
}
},
{ deep: true },
);
const handleClose = () => {
open.value = false;
em('close', false);
};
const getPath = (codeDir: string) => {
runtime.codeDir = codeDir;
getScripts();
};
const getScripts = () => {
GetNodeScripts({ codeDir: runtime.codeDir }).then((res) => {
scripts.value = res.data;
if (scripts.value.length > 0) {
runtime.params['EXEC_SCRIPT'] = scripts.value[0].script;
}
});
};
const searchApp = (appId: number) => {
SearchApp(appReq).then((res) => {
apps.value = res.data.items || [];
if (res.data && res.data.items && res.data.items.length > 0) {
if (appId == null) {
runtime.appId = res.data.items[0].id;
getApp(res.data.items[0].key, mode.value);
} else {
res.data.items.forEach((item) => {
if (item.id === appId) {
getApp(item.key, mode.value);
}
});
}
}
});
};
const changeApp = (appId: number) => {
for (const app of apps.value) {
if (app.id === appId) {
getApp(app.key, mode.value);
break;
}
}
};
const changeVersion = () => {
loading.value = true;
GetAppDetail(runtime.appId, runtime.version, 'runtime')
.then((res) => {
runtime.appDetailId = res.data.id;
})
.finally(() => {
loading.value = false;
});
};
const getApp = (appkey: string, mode: string) => {
GetApp(appkey).then((res) => {
appVersions.value = res.data.versions || [];
if (res.data.versions.length > 0) {
runtime.version = res.data.versions[0];
if (mode === 'create') {
changeVersion();
}
}
});
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
if (mode.value == 'create') {
loading.value = true;
CreateRuntime(runtime)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
} else {
loading.value = true;
UpdateRuntime(runtime)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
}
});
};
const getRuntime = async (id: number) => {
try {
const res = await GetRuntime(id);
const data = res.data;
Object.assign(runtime, {
id: data.id,
name: data.name,
appDetailId: data.appDetailId,
image: data.image,
params: {},
type: data.type,
resource: data.resource,
appId: data.appId,
version: data.version,
rebuild: true,
source: data.source,
});
editParams.value = data.appParams;
if (mode.value == 'create') {
searchApp(data.appId);
}
} catch (error) {}
};
const acceptParams = async (props: OperateRrops) => {
mode.value = props.mode;
if (props.mode === 'create') {
Object.assign(runtime, initData(props.type));
searchApp(null);
} else {
searchApp(null);
getRuntime(props.id);
}
open.value = true;
};
defineExpose({
acceptParams,
});
</script>

View File

@ -0,0 +1,164 @@
<template>
<div>
<RouterMenu />
<LayoutContent :title="'Node.js'" v-loading="loading">
<template #toolbar>
<el-button type="primary" @click="openCreate">
{{ $t('runtime.create') }}
</el-button>
</template>
<template #main>
<ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()">
<el-table-column :label="$t('commons.table.name')" fix prop="name" min-width="120px">
<template #default="{ row }">
<Tooltip :text="row.name" @click="openDetail(row)" />
</template>
</el-table-column>
<el-table-column :label="$t('runtime.codeDir')" prop="codeDir">
<template #default="{ row }">
<el-button type="primary" link @click="toFolder(row.codeDir)">
<el-icon>
<FolderOpened />
</el-icon>
</el-button>
</template>
</el-table-column>
<el-table-column :label="$t('runtime.version')" prop="version"></el-table-column>
<el-table-column
:label="$t('runtime.externalPort')"
prop="params.PANEL_APP_PORT_HTTP"
></el-table-column>
<el-table-column :label="$t('commons.table.status')" prop="status">
<template #default="{ row }">
<el-popover
v-if="row.status === 'error'"
placement="bottom"
:width="400"
trigger="hover"
:content="row.message"
>
<template #reference>
<Status :key="row.status" :status="row.status"></Status>
</template>
</el-popover>
<div v-else>
<Status :key="row.status" :status="row.status"></Status>
</div>
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFormat"
show-overflow-tooltip
min-width="120"
fix
/>
<fu-table-operations
:ellipsis="10"
width="120px"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
</template>
</LayoutContent>
<OperateNode ref="operateRef" @close="search" />
<Delete ref="deleteRef" @close="search()" />
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { Runtime } from '@/api/interface/runtime';
import { SearchRuntimes } from '@/api/modules/runtime';
import { dateFormat } from '@/utils/util';
import OperateNode from '@/views/website/runtime/node/operate/index.vue';
import Status from '@/components/status/index.vue';
import Delete from '@/views/website/runtime/delete/index.vue';
import i18n from '@/lang';
import RouterMenu from '../index.vue';
import router from '@/routers/router';
const paginationConfig = reactive({
cacheSizeKey: 'runtime-page-size',
currentPage: 1,
pageSize: 10,
total: 0,
});
const req = reactive<Runtime.RuntimeReq>({
name: '',
page: 1,
pageSize: 40,
type: 'node',
});
let timer: NodeJS.Timer | null = null;
const buttons = [
{
label: i18n.global.t('commons.button.edit'),
click: function (row: Runtime.Runtime) {
openDetail(row);
},
disabled: function (row: Runtime.Runtime) {
return row.status === 'starting' || row.status === 'recreating';
},
},
{
label: i18n.global.t('commons.button.delete'),
click: function (row: Runtime.Runtime) {
openDelete(row);
},
},
];
const loading = ref(false);
const items = ref<Runtime.RuntimeDTO[]>([]);
const operateRef = ref();
const deleteRef = ref();
const search = async () => {
req.page = paginationConfig.currentPage;
req.pageSize = paginationConfig.pageSize;
loading.value = true;
try {
const res = await SearchRuntimes(req);
items.value = res.data.items;
paginationConfig.total = res.data.total;
} catch (error) {
} finally {
loading.value = false;
}
};
const openCreate = () => {
operateRef.value.acceptParams({ type: 'node', mode: 'create' });
};
const openDetail = (row: Runtime.Runtime) => {
operateRef.value.acceptParams({ type: row.type, mode: 'edit', id: row.id });
};
const openDelete = async (row: Runtime.Runtime) => {
deleteRef.value.acceptParams(row.id, row.name);
};
const toFolder = (folder: string) => {
router.push({ path: '/hosts/files', query: { path: folder } });
};
onMounted(() => {
search();
timer = setInterval(() => {
search();
}, 10000 * 3);
});
onUnmounted(() => {
clearInterval(Number(timer));
timer = null;
});
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,351 @@
<template>
<el-drawer :close-on-click-modal="false" v-model="open" size="50%">
<template #header>
<DrawerHeader
:header="$t('runtime.' + mode)"
:hideResource="mode == 'create'"
:resource="runtime.name"
:back="handleClose"
/>
</template>
<el-row v-loading="loading">
<el-col :span="22" :offset="1">
<el-form
ref="runtimeForm"
label-position="top"
:model="runtime"
label-width="125px"
:rules="rules"
:validate-on-rule-change="false"
>
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input :disabled="mode === 'edit'" v-model="runtime.name"></el-input>
</el-form-item>
<el-form-item :label="$t('runtime.app')" prop="appID">
<el-row :gutter="20">
<el-col :span="12">
<el-select
v-model="runtime.appID"
:disabled="mode === 'edit'"
@change="changeApp(runtime.appID)"
>
<el-option
v-for="(app, index) in apps"
:key="index"
:label="app.name"
:value="app.id"
></el-option>
</el-select>
</el-col>
<el-col :span="12">
<el-select
v-model="runtime.version"
:disabled="mode === 'edit'"
@change="changeVersion()"
>
<el-option
v-for="(version, index) in appVersions"
:key="index"
:label="version"
:value="version"
></el-option>
</el-select>
</el-col>
</el-row>
</el-form-item>
<el-form-item :label="$t('runtime.codeDir')" prop="codeDir">
<el-input v-model.trim="runtime.codeDir" :disabled="mode === 'edit'">
<template #prepend>
<FileList
:disabled="mode === 'edit'"
:path="runtime.codeDir"
@choose="getPath"
:dir="true"
></FileList>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('runtime.runScript')" prop="params.EXEC_SCRIPT">
<el-select v-model="runtime.params['EXEC_SCRIPT']">
<el-option
v-for="(script, index) in scripts"
:key="index"
:label="script.name + ' 【 ' + script.script + ' 】'"
:value="script.name"
>
<el-row :gutter="10">
<el-col :span="4">{{ script.name }}</el-col>
<el-col :span="10">{{ ' 【 ' + script.script + ' 】' }}</el-col>
</el-row>
</el-option>
</el-select>
<span class="input-help">{{ $t('runtime.runScriptHelper') }}</span>
</el-form-item>
<el-row :gutter="20">
<el-col :span="10">
<el-form-item :label="$t('runtime.appPort')" prop="params.NODE_APP_PORT">
<el-input v-model.number="runtime.params['NODE_APP_PORT']" />
<span class="input-help">{{ $t('runtime.appPortHelper') }}</span>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item :label="$t('runtime.externalPort')" prop="params.PANEL_APP_PORT_HTTP">
<el-input v-model.number="runtime.params['PANEL_APP_PORT_HTTP']" />
<span class="input-help">{{ $t('runtime.externalPortHelper') }}</span>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item :label="$t('app.allowPort')" prop="params.HOST_IP">
<el-select v-model="runtime.params['HOST_IP']">
<el-option :label="$t('runtime.open')" value="0.0.0.0"></el-option>
<el-option :label="$t('runtime.close')" value="127.0.0.1"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="$t('runtime.packageManager')" prop="params.PACKAGE_MANAGER">
<el-select v-model="runtime.params['PACKAGE_MANAGER']">
<el-option label="npm" value="npm"></el-option>
<el-option label="yarn" value="yarn"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('app.containerName')" prop="params.CONTAINER_NAME">
<el-input v-model.trim="runtime.params['CONTAINER_NAME']"></el-input>
</el-form-item>
</el-form>
</el-col>
</el-row>
<template #footer>
<span>
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(runtimeForm)" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { App } from '@/api/interface/app';
import { Runtime } from '@/api/interface/runtime';
import { GetApp, GetAppDetail, SearchApp } from '@/api/modules/app';
import { CreateRuntime, GetNodeScripts, GetRuntime, UpdateRuntime } from '@/api/modules/runtime';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { reactive, ref, watch } from 'vue';
import DrawerHeader from '@/components/drawer-header/index.vue';
interface OperateRrops {
id?: number;
mode: string;
type: string;
}
const open = ref(false);
const apps = ref<App.App[]>([]);
const runtimeForm = ref<FormInstance>();
const loading = ref(false);
const mode = ref('create');
const editParams = ref<App.InstallParams[]>();
const appVersions = ref<string[]>([]);
const appReq = reactive({
type: 'node',
page: 1,
pageSize: 20,
});
const initData = (type: string) => ({
name: '',
appDetailID: undefined,
image: '',
params: {
PACKAGE_MANAGER: 'npm',
HOST_IP: '0.0.0.0',
},
type: type,
resource: 'appstore',
rebuild: false,
codeDir: '/',
});
let runtime = reactive<Runtime.RuntimeCreate>(initData('node'));
const rules = ref<any>({
name: [Rules.appName],
appID: [Rules.requiredSelect],
codeDir: [Rules.requiredInput],
params: {
NODE_APP_PORT: [Rules.requiredInput, Rules.port],
PANEL_APP_PORT_HTTP: [Rules.requiredInput, Rules.port],
PACKAGE_MANAGER: [Rules.requiredSelect],
HOST_IP: [Rules.requiredSelect],
EXEC_SCRIPT: [Rules.requiredSelect],
CONTAINER_NAME: [Rules.requiredInput],
},
});
const scripts = ref<Runtime.NodeScripts[]>([]);
const em = defineEmits(['close']);
watch(
() => runtime.params['NODE_APP_PORT'],
(newVal) => {
if (newVal && mode.value == 'create') {
runtime.params['PANEL_APP_PORT_HTTP'] = newVal;
}
},
{ deep: true },
);
watch(
() => runtime.name,
(newVal) => {
if (newVal) {
runtime.params['CONTAINER_NAME'] = newVal;
}
},
{ deep: true },
);
const handleClose = () => {
open.value = false;
em('close', false);
runtimeForm.value?.resetFields();
};
const getPath = (codeDir: string) => {
runtime.codeDir = codeDir;
getScripts();
};
const getScripts = () => {
GetNodeScripts({ codeDir: runtime.codeDir }).then((res) => {
scripts.value = res.data;
if (mode.value == 'create' && scripts.value.length > 0) {
runtime.params['EXEC_SCRIPT'] = scripts.value[0].name;
}
});
};
const searchApp = (appID: number) => {
SearchApp(appReq).then((res) => {
apps.value = res.data.items || [];
if (res.data && res.data.items && res.data.items.length > 0) {
if (appID == null) {
runtime.appID = res.data.items[0].id;
getApp(res.data.items[0].key, mode.value);
} else {
res.data.items.forEach((item) => {
if (item.id === appID) {
getApp(item.key, mode.value);
}
});
}
}
});
};
const changeApp = (appID: number) => {
for (const app of apps.value) {
if (app.id === appID) {
getApp(app.key, mode.value);
break;
}
}
};
const changeVersion = () => {
loading.value = true;
GetAppDetail(runtime.appID, runtime.version, 'runtime')
.then((res) => {
runtime.appDetailID = res.data.id;
})
.finally(() => {
loading.value = false;
});
};
const getApp = (appkey: string, mode: string) => {
GetApp(appkey).then((res) => {
appVersions.value = res.data.versions || [];
if (res.data.versions.length > 0) {
runtime.version = res.data.versions[0];
if (mode === 'create') {
changeVersion();
}
}
});
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
if (mode.value == 'create') {
loading.value = true;
CreateRuntime(runtime)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
} else {
loading.value = true;
UpdateRuntime(runtime)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
}
});
};
const getRuntime = async (id: number) => {
try {
const res = await GetRuntime(id);
const data = res.data;
Object.assign(runtime, {
id: data.id,
name: data.name,
appDetailId: data.appDetailID,
image: data.image,
type: data.type,
resource: data.resource,
appID: data.appID,
version: data.version,
rebuild: true,
source: data.source,
params: data.params,
codeDir: data.codeDir,
});
editParams.value = data.appParams;
if (mode.value == 'edit') {
searchApp(data.appID);
}
getScripts();
} catch (error) {}
};
const acceptParams = async (props: OperateRrops) => {
mode.value = props.mode;
scripts.value = [];
if (props.mode === 'create') {
Object.assign(runtime, initData(props.type));
searchApp(null);
} else {
getRuntime(props.id);
}
open.value = true;
};
defineExpose({
acceptParams,
});
</script>

View File

@ -35,9 +35,9 @@
<el-row :gutter="20">
<el-col :span="12">
<el-select
v-model="runtime.appId"
v-model="runtime.appID"
:disabled="mode === 'edit'"
@change="changeApp(runtime.appId)"
@change="changeApp(runtime.appID)"
>
<el-option
v-for="(app, index) in apps"
@ -178,7 +178,7 @@ const appReq = reactive({
});
const initData = (type: string) => ({
name: '',
appDetailId: undefined,
appDetailID: undefined,
image: '',
params: {},
type: type,
@ -192,7 +192,7 @@ let runtime = reactive<Runtime.RuntimeCreate>(initData('php'));
const rules = ref<any>({
name: [Rules.appName],
resource: [Rules.requiredInput],
appId: [Rules.requiredSelect],
appID: [Rules.requiredSelect],
version: [Rules.requiredInput, Rules.paramCommon],
image: [Rules.requiredInput, Rules.imageName],
source: [Rules.requiredSelect],
@ -238,7 +238,7 @@ const handleClose = () => {
const changeResource = (resource: string) => {
if (resource === 'local') {
runtime.appDetailId = undefined;
runtime.appDetailID = undefined;
runtime.version = '';
runtime.params = {};
runtime.image = '';
@ -253,7 +253,7 @@ const searchApp = (appId: number) => {
apps.value = res.data.items || [];
if (res.data && res.data.items && res.data.items.length > 0) {
if (appId == null) {
runtime.appId = res.data.items[0].id;
runtime.appID = res.data.items[0].id;
getApp(res.data.items[0].key, mode.value);
} else {
res.data.items.forEach((item) => {
@ -279,9 +279,9 @@ const changeApp = (appId: number) => {
const changeVersion = () => {
loading.value = true;
initParam.value = false;
GetAppDetail(runtime.appId, runtime.version, 'runtime')
GetAppDetail(runtime.appID, runtime.version, 'runtime')
.then((res) => {
runtime.appDetailId = res.data.id;
runtime.appDetailID = res.data.id;
runtime.image = res.data.image + ':' + runtime.version;
appParams.value = res.data.params;
initParam.value = true;
@ -342,19 +342,19 @@ const getRuntime = async (id: number) => {
Object.assign(runtime, {
id: data.id,
name: data.name,
appDetailId: data.appDetailId,
appDetailID: data.appDetailID,
image: data.image,
params: {},
type: data.type,
resource: data.resource,
appId: data.appId,
appID: data.appID,
version: data.version,
rebuild: true,
source: data.source,
});
editParams.value = data.appParams;
if (mode.value == 'create') {
searchApp(data.appId);
searchApp(data.appID);
} else {
initParam.value = true;
}

View File

@ -0,0 +1,156 @@
<template>
<div>
<RouterMenu />
<LayoutContent :title="'PHP'" v-loading="loading">
<template #toolbar>
<el-button type="primary" @click="openCreate">
{{ $t('runtime.create') }}
</el-button>
</template>
<template #main>
<ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()">
<el-table-column :label="$t('commons.table.name')" fix prop="name" min-width="120px">
<template #default="{ row }">
<Tooltip :text="row.name" @click="openDetail(row)" />
</template>
</el-table-column>
<el-table-column :label="$t('runtime.resource')" prop="resource">
<template #default="{ row }">
<span>{{ $t('runtime.' + toLowerCase(row.resource)) }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('runtime.version')" prop="version"></el-table-column>
<el-table-column :label="$t('runtime.image')" prop="image" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('commons.table.status')" prop="status">
<template #default="{ row }">
<el-popover
v-if="row.status === 'error'"
placement="bottom"
:width="400"
trigger="hover"
:content="row.message"
>
<template #reference>
<Status :key="row.status" :status="row.status"></Status>
</template>
</el-popover>
<div v-else>
<Status :key="row.status" :status="row.status"></Status>
</div>
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFormat"
show-overflow-tooltip
min-width="120"
fix
/>
<fu-table-operations
:ellipsis="10"
width="120px"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
</template>
</LayoutContent>
<CreateRuntime ref="createRef" @close="search" />
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { Runtime } from '@/api/interface/runtime';
import { DeleteRuntime, SearchRuntimes } from '@/api/modules/runtime';
import { dateFormat, toLowerCase } from '@/utils/util';
import CreateRuntime from '@/views/website/runtime/php/create/index.vue';
import Status from '@/components/status/index.vue';
import i18n from '@/lang';
import { useDeleteData } from '@/hooks/use-delete-data';
import RouterMenu from '../index.vue';
const paginationConfig = reactive({
cacheSizeKey: 'runtime-page-size',
currentPage: 1,
pageSize: 10,
total: 0,
});
let req = reactive<Runtime.RuntimeReq>({
name: '',
page: 1,
pageSize: 40,
type: 'php',
});
let timer: NodeJS.Timer | null = null;
const buttons = [
{
label: i18n.global.t('commons.button.edit'),
click: function (row: Runtime.Runtime) {
openDetail(row);
},
disabled: function (row: Runtime.Runtime) {
return row.status === 'building';
},
},
{
label: i18n.global.t('commons.button.delete'),
click: function (row: Runtime.Runtime) {
openDelete(row);
},
},
];
const loading = ref(false);
const items = ref<Runtime.RuntimeDTO[]>([]);
const createRef = ref();
const search = async () => {
req.page = paginationConfig.currentPage;
req.pageSize = paginationConfig.pageSize;
loading.value = true;
try {
const res = await SearchRuntimes(req);
items.value = res.data.items;
paginationConfig.total = res.data.total;
} catch (error) {
} finally {
loading.value = false;
}
};
const openCreate = () => {
createRef.value.acceptParams({ type: 'php', mode: 'create' });
};
const openDetail = (row: Runtime.Runtime) => {
createRef.value.acceptParams({ type: row.type, mode: 'edit', id: row.id });
};
const openDelete = async (row: Runtime.Runtime) => {
await useDeleteData(DeleteRuntime, { id: row.id }, 'commons.msg.delete');
search();
};
onMounted(() => {
search();
timer = setInterval(() => {
search();
}, 10000 * 3);
});
onUnmounted(() => {
clearInterval(Number(timer));
timer = null;
});
</script>
<style lang="scss" scoped>
.open-warn {
color: $primary-color;
cursor: pointer;
}
</style>