mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-31 14:08:06 +08:00
feat: compose 功能实现
This commit is contained in:
parent
56c3c2f4cb
commit
3ca1e97469
@ -90,6 +90,7 @@ func (b *BaseApi) UpdateComposeTemplate(c *gin.Context) {
|
|||||||
|
|
||||||
upMap := make(map[string]interface{})
|
upMap := make(map[string]interface{})
|
||||||
upMap["from"] = req.From
|
upMap["from"] = req.From
|
||||||
|
upMap["path"] = req.Path
|
||||||
upMap["content"] = req.Content
|
upMap["content"] = req.Content
|
||||||
upMap["description"] = req.Description
|
upMap["description"] = req.Description
|
||||||
if err := composeTemplateService.Update(id, upMap); err != nil {
|
if err := composeTemplateService.Update(id, upMap); err != nil {
|
||||||
|
@ -32,6 +32,64 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) SearchCompose(c *gin.Context) {
|
||||||
|
var req dto.PageInfo
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := global.VALID.Struct(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
total, list, err := containerService.PageCompose(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, dto.PageResult{
|
||||||
|
Items: list,
|
||||||
|
Total: total,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) CreateCompose(c *gin.Context) {
|
||||||
|
var req dto.ComposeCreate
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := global.VALID.Struct(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := containerService.CreateCompose(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) OperatorCompose(c *gin.Context) {
|
||||||
|
var req dto.ComposeOperation
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := global.VALID.Struct(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := containerService.ComposeOperation(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BaseApi) ContainerCreate(c *gin.Context) {
|
func (b *BaseApi) ContainerCreate(c *gin.Context) {
|
||||||
var req dto.ContainerCreate
|
var req dto.ContainerCreate
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
@ -6,12 +6,14 @@ type ComposeTemplateCreate struct {
|
|||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
From string `json:"from" validate:"required,oneof=edit path"`
|
From string `json:"from" validate:"required,oneof=edit path"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Path string `json:"path"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ComposeTemplateUpdate struct {
|
type ComposeTemplateUpdate struct {
|
||||||
From string `json:"from" validate:"required,oneof=edit path"`
|
From string `json:"from" validate:"required,oneof=edit path"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Path string `json:"path"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,5 +23,6 @@ type ComposeTemplateInfo struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
From string `json:"from"`
|
From string `json:"from"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Path string `json:"path"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import "time"
|
|||||||
|
|
||||||
type PageContainer struct {
|
type PageContainer struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
Status string `json:"status" validate:"required,oneof=all running"`
|
Filters string `json:"filters"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InspectReq struct {
|
type InspectReq struct {
|
||||||
@ -66,7 +66,7 @@ type ContainerLog struct {
|
|||||||
|
|
||||||
type ContainerOperation struct {
|
type ContainerOperation struct {
|
||||||
ContainerID string `json:"containerID" validate:"required"`
|
ContainerID string `json:"containerID" validate:"required"`
|
||||||
Operation string `json:"operation" validate:"required,oneof=start stop reStart kill pause unPause reName remove"`
|
Operation string `json:"operation" validate:"required,oneof=start stop restart kill pause unpause rename remove"`
|
||||||
NewName string `json:"newName"`
|
NewName string `json:"newName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,3 +108,30 @@ type VolumeCreat struct {
|
|||||||
type BatchDelete struct {
|
type BatchDelete struct {
|
||||||
Ids []string `json:"ids" validate:"required"`
|
Ids []string `json:"ids" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ComposeInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
ContainerNumber int `json:"containerNumber"`
|
||||||
|
ConfigFile string `json:"configFile"`
|
||||||
|
Workdir string `json:"workdir"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Containers []ComposeContainer `json:"containers"`
|
||||||
|
}
|
||||||
|
type ComposeContainer struct {
|
||||||
|
ContainerID string `json:"containerID"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreateTime string `json:"createTime"`
|
||||||
|
State string `json:"state"`
|
||||||
|
}
|
||||||
|
type ComposeCreate struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
From string `json:"from" validate:"required,oneof=edit path template"`
|
||||||
|
File string `json:"file"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Template uint `json:"template"`
|
||||||
|
}
|
||||||
|
type ComposeOperation struct {
|
||||||
|
Path string `json:"path" validate:"required"`
|
||||||
|
Operation string `json:"operation" validate:"required,oneof=up stop pause unpause restart down"`
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ type ComposeTemplate struct {
|
|||||||
|
|
||||||
Name string `gorm:"type:varchar(64);not null;unique" json:"name"`
|
Name string `gorm:"type:varchar(64);not null;unique" json:"name"`
|
||||||
From string `gorm:"type:varchar(64);not null" json:"from"`
|
From string `gorm:"type:varchar(64);not null" json:"from"`
|
||||||
Description string `gorm:"type:varchar(256);" json:"description"`
|
Description string `gorm:"type:varchar(256)" json:"description"`
|
||||||
|
Path string `gorm:"type:varchar(64)" json:"path"`
|
||||||
Content string `gorm:"type:longtext" json:"content"`
|
Content string `gorm:"type:longtext" json:"content"`
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/app/dto"
|
"github.com/1Panel-dev/1Panel/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/constant"
|
"github.com/1Panel-dev/1Panel/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/global"
|
||||||
"github.com/1Panel-dev/1Panel/utils/docker"
|
"github.com/1Panel-dev/1Panel/utils/docker"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
@ -26,11 +30,18 @@ import (
|
|||||||
|
|
||||||
type ContainerService struct{}
|
type ContainerService struct{}
|
||||||
|
|
||||||
|
const composeProjectLabel = "com.docker.compose.project"
|
||||||
|
const composeConfigLabel = "com.docker.compose.project.config_files"
|
||||||
|
const composeWorkdirLabel = "com.docker.compose.project.working_dir"
|
||||||
|
|
||||||
type IContainerService interface {
|
type IContainerService interface {
|
||||||
Page(req dto.PageContainer) (int64, interface{}, error)
|
Page(req dto.PageContainer) (int64, interface{}, error)
|
||||||
PageNetwork(req dto.PageInfo) (int64, interface{}, error)
|
PageNetwork(req dto.PageInfo) (int64, interface{}, error)
|
||||||
PageVolume(req dto.PageInfo) (int64, interface{}, error)
|
PageVolume(req dto.PageInfo) (int64, interface{}, error)
|
||||||
ListVolume() ([]dto.Options, error)
|
ListVolume() ([]dto.Options, error)
|
||||||
|
PageCompose(req dto.PageInfo) (int64, interface{}, error)
|
||||||
|
CreateCompose(req dto.ComposeCreate) error
|
||||||
|
ComposeOperation(req dto.ComposeOperation) error
|
||||||
ContainerCreate(req dto.ContainerCreate) error
|
ContainerCreate(req dto.ContainerCreate) error
|
||||||
ContainerOperation(req dto.ContainerOperation) error
|
ContainerOperation(req dto.ContainerOperation) error
|
||||||
ContainerLogs(param dto.ContainerLog) (string, error)
|
ContainerLogs(param dto.ContainerLog) (string, error)
|
||||||
@ -56,7 +67,12 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
list, err = client.ContainerList(context.Background(), types.ContainerListOptions{All: req.Status == "all"})
|
options := types.ContainerListOptions{All: true}
|
||||||
|
if len(req.Filters) != 0 {
|
||||||
|
options.Filters = filters.NewArgs()
|
||||||
|
options.Filters.Add("label", req.Filters)
|
||||||
|
}
|
||||||
|
list, err = client.ContainerList(context.Background(), options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
@ -85,6 +101,128 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
|
|||||||
return int64(total), backDatas, nil
|
return int64(total), backDatas, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ContainerService) PageCompose(req dto.PageInfo) (int64, interface{}, error) {
|
||||||
|
var (
|
||||||
|
records []dto.ComposeInfo
|
||||||
|
BackDatas []dto.ComposeInfo
|
||||||
|
)
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
options := types.ContainerListOptions{All: true}
|
||||||
|
options.Filters = filters.NewArgs()
|
||||||
|
options.Filters.Add("label", composeProjectLabel)
|
||||||
|
|
||||||
|
list, err := client.ContainerList(context.Background(), options)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
composeMap := make(map[string]dto.ComposeInfo)
|
||||||
|
for _, container := range list {
|
||||||
|
if name, ok := container.Labels[composeProjectLabel]; ok {
|
||||||
|
containerItem := dto.ComposeContainer{
|
||||||
|
ContainerID: container.ID,
|
||||||
|
Name: container.Names[0][1:],
|
||||||
|
State: container.State,
|
||||||
|
CreateTime: time.Unix(container.Created, 0).Format("2006-01-02 15:04:05"),
|
||||||
|
}
|
||||||
|
if compose, has := composeMap[name]; has {
|
||||||
|
compose.ContainerNumber++
|
||||||
|
compose.Containers = append(compose.Containers, containerItem)
|
||||||
|
composeMap[name] = compose
|
||||||
|
} else {
|
||||||
|
config := container.Labels[composeConfigLabel]
|
||||||
|
workdir := container.Labels[composeWorkdirLabel]
|
||||||
|
composeItem := dto.ComposeInfo{
|
||||||
|
ContainerNumber: 1,
|
||||||
|
CreatedAt: time.Unix(container.Created, 0).Format("2006-01-02 15:04:05"),
|
||||||
|
ConfigFile: config,
|
||||||
|
Workdir: workdir,
|
||||||
|
Containers: []dto.ComposeContainer{containerItem},
|
||||||
|
}
|
||||||
|
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
||||||
|
composeItem.Path = config
|
||||||
|
} else {
|
||||||
|
composeItem.Path = workdir
|
||||||
|
}
|
||||||
|
composeMap[name] = composeItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key, value := range composeMap {
|
||||||
|
value.Name = key
|
||||||
|
records = append(records, value)
|
||||||
|
}
|
||||||
|
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||||
|
if start > total {
|
||||||
|
BackDatas = make([]dto.ComposeInfo, 0)
|
||||||
|
} else {
|
||||||
|
if end >= total {
|
||||||
|
end = total
|
||||||
|
}
|
||||||
|
BackDatas = records[start:end]
|
||||||
|
}
|
||||||
|
return int64(total), BackDatas, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
|
||||||
|
if req.From == "template" {
|
||||||
|
template, err := composeRepo.Get(commonRepo.WithByID(req.Template))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.From = template.From
|
||||||
|
if req.From == "edit" {
|
||||||
|
req.File = template.Content
|
||||||
|
} else {
|
||||||
|
req.Path = template.Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.From == "edit" {
|
||||||
|
dir := fmt.Sprintf("%s/%s", constant.TmpComposeBuildDir, req.Name)
|
||||||
|
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
|
||||||
|
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path := fmt.Sprintf("%s/docker-compose.yml", dir)
|
||||||
|
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
write := bufio.NewWriter(file)
|
||||||
|
_, _ = write.WriteString(string(req.File))
|
||||||
|
write.Flush()
|
||||||
|
req.Path = path
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
cmd := exec.Command("docker-compose", "-f", req.Path, "up", "-d")
|
||||||
|
stdout, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Debugf("docker-compose up %s failed, err: %v", req.Name, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
global.LOG.Debugf("docker-compose up %s successful, logs: %v", req.Name, string(stdout))
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
||||||
|
cmd := exec.Command("docker-compose", "-f", req.Path, req.Operation)
|
||||||
|
stdout, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
global.LOG.Debugf("docker-compose %s %s successful: logs: %v", req.Operation, req.Path, string(stdout))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
|
func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -9,11 +9,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/app/dto"
|
|
||||||
"github.com/1Panel-dev/1Panel/constant"
|
"github.com/1Panel-dev/1Panel/constant"
|
||||||
"github.com/1Panel-dev/1Panel/utils/docker"
|
"github.com/1Panel-dev/1Panel/utils/docker"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/pkg/archive"
|
"github.com/docker/docker/pkg/archive"
|
||||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
@ -96,31 +96,11 @@ func TestNetwork(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
res, err := client.ContainerStatsOneShot(context.TODO(), "30e4d3395b87")
|
options := types.ContainerListOptions{All: true}
|
||||||
if err != nil {
|
options.Filters = filters.NewArgs()
|
||||||
fmt.Println(err)
|
options.Filters.Add("label", "maintainer")
|
||||||
}
|
ss, _ := client.ContainerList(context.TODO(), options)
|
||||||
defer res.Body.Close()
|
fmt.Println(ss)
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
var state *types.StatsJSON
|
|
||||||
if err := json.Unmarshal(body, &state); err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(string(body))
|
|
||||||
|
|
||||||
var data dto.ContainterStats
|
|
||||||
previousCPU := state.PreCPUStats.CPUUsage.TotalUsage
|
|
||||||
previousSystem := state.PreCPUStats.SystemUsage
|
|
||||||
data.CPUPercent = calculateCPUPercentUnix(previousCPU, previousSystem, state)
|
|
||||||
data.IORead, data.IOWrite = calculateBlockIO(state.BlkioStats)
|
|
||||||
data.Memory = float64(state.MemoryStats.Usage)
|
|
||||||
data.NetworkRX, data.NetworkTX = calculateNetwork(state.Networks)
|
|
||||||
fmt.Println(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContainer(t *testing.T) {
|
func TestContainer(t *testing.T) {
|
||||||
|
@ -3,13 +3,18 @@ package constant
|
|||||||
const (
|
const (
|
||||||
ContainerOpStart = "start"
|
ContainerOpStart = "start"
|
||||||
ContainerOpStop = "stop"
|
ContainerOpStop = "stop"
|
||||||
ContainerOpRestart = "reStart"
|
ContainerOpRestart = "restart"
|
||||||
ContainerOpKill = "kill"
|
ContainerOpKill = "kill"
|
||||||
ContainerOpPause = "pause"
|
ContainerOpPause = "pause"
|
||||||
ContainerOpUnpause = "unPause"
|
ContainerOpUnpause = "unpause"
|
||||||
ContainerOpRename = "reName"
|
ContainerOpRename = "rename"
|
||||||
ContainerOpRemove = "remove"
|
ContainerOpRemove = "remove"
|
||||||
|
|
||||||
|
ComposeOpStop = "stop"
|
||||||
|
ComposeOpRestart = "restart"
|
||||||
|
ComposeOpRemove = "remove"
|
||||||
|
|
||||||
DaemonJsonDir = "/System/Volumes/Data/Users/slooop/.docker/daemon.json"
|
DaemonJsonDir = "/System/Volumes/Data/Users/slooop/.docker/daemon.json"
|
||||||
TmpDockerBuildDir = "/opt/1Panel/build"
|
TmpDockerBuildDir = "/opt/1Panel/data/docker/build"
|
||||||
|
TmpComposeBuildDir = "/opt/1Panel/data/docker/compose"
|
||||||
)
|
)
|
||||||
|
@ -33,11 +33,15 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
|
|||||||
withRecordRouter.POST("/repo", baseApi.CreateRepo)
|
withRecordRouter.POST("/repo", baseApi.CreateRepo)
|
||||||
withRecordRouter.POST("/repo/del", baseApi.DeleteRepo)
|
withRecordRouter.POST("/repo/del", baseApi.DeleteRepo)
|
||||||
|
|
||||||
baRouter.POST("/compose/search", baseApi.SearchComposeTemplate)
|
baRouter.POST("/compose/search", baseApi.SearchCompose)
|
||||||
baRouter.PUT("/compose/:id", baseApi.UpdateComposeTemplate)
|
baRouter.POST("/compose/up", baseApi.CreateCompose)
|
||||||
baRouter.GET("/compose", baseApi.ListComposeTemplate)
|
baRouter.POST("/compose/operate", baseApi.OperatorCompose)
|
||||||
withRecordRouter.POST("/compose", baseApi.CreateComposeTemplate)
|
|
||||||
withRecordRouter.POST("/compose/del", baseApi.DeleteComposeTemplate)
|
baRouter.POST("/template/search", baseApi.SearchComposeTemplate)
|
||||||
|
baRouter.PUT("/template/:id", baseApi.UpdateComposeTemplate)
|
||||||
|
baRouter.GET("/template", baseApi.ListComposeTemplate)
|
||||||
|
withRecordRouter.POST("/template", baseApi.CreateComposeTemplate)
|
||||||
|
withRecordRouter.POST("/template/del", baseApi.DeleteComposeTemplate)
|
||||||
|
|
||||||
baRouter.POST("/image/search", baseApi.SearchImage)
|
baRouter.POST("/image/search", baseApi.SearchImage)
|
||||||
baRouter.GET("/image", baseApi.ListImage)
|
baRouter.GET("/image", baseApi.ListImage)
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
|
import { ReqPage } from '.';
|
||||||
|
|
||||||
export namespace Container {
|
export namespace Container {
|
||||||
export interface ContainerOperate {
|
export interface ContainerOperate {
|
||||||
containerID: string;
|
containerID: string;
|
||||||
operation: string;
|
operation: string;
|
||||||
newName: string;
|
newName: string;
|
||||||
}
|
}
|
||||||
|
export interface ContainerSearch extends ReqPage {
|
||||||
|
filters: string;
|
||||||
|
}
|
||||||
export interface ContainerCreate {
|
export interface ContainerCreate {
|
||||||
name: string;
|
name: string;
|
||||||
image: string;
|
image: string;
|
||||||
@ -161,16 +166,46 @@ export namespace Container {
|
|||||||
downloadUrl: string;
|
downloadUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ComposeInfo {
|
||||||
|
name: string;
|
||||||
|
createdAt: string;
|
||||||
|
containerNumber: number;
|
||||||
|
configFile: string;
|
||||||
|
workdir: string;
|
||||||
|
path: string;
|
||||||
|
containers: Array<ComposeContainer>;
|
||||||
|
expand: boolean;
|
||||||
|
}
|
||||||
|
export interface ComposeContainer {
|
||||||
|
name: string;
|
||||||
|
createTime: string;
|
||||||
|
containerID: string;
|
||||||
|
state: string;
|
||||||
|
}
|
||||||
|
export interface ComposeCreate {
|
||||||
|
name: string;
|
||||||
|
from: string;
|
||||||
|
file: string;
|
||||||
|
path: string;
|
||||||
|
template: number;
|
||||||
|
}
|
||||||
|
export interface ComposeOpration {
|
||||||
|
operation: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TemplateCreate {
|
export interface TemplateCreate {
|
||||||
name: string;
|
name: string;
|
||||||
from: string;
|
from: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
path: string;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
export interface TemplateUpdate {
|
export interface TemplateUpdate {
|
||||||
id: number;
|
id: number;
|
||||||
from: string;
|
from: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
path: string;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
export interface TemplateInfo {
|
export interface TemplateInfo {
|
||||||
@ -179,6 +214,7 @@ export namespace Container {
|
|||||||
name: string;
|
name: string;
|
||||||
from: string;
|
from: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
path: string;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import http from '@/api';
|
|||||||
import { ResPage, ReqPage } from '../interface';
|
import { ResPage, ReqPage } from '../interface';
|
||||||
import { Container } from '../interface/container';
|
import { Container } from '../interface/container';
|
||||||
|
|
||||||
export const searchContainer = (params: ReqPage) => {
|
export const searchContainer = (params: Container.ContainerSearch) => {
|
||||||
return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params);
|
return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params);
|
||||||
};
|
};
|
||||||
export const createContainer = (params: Container.ContainerCreate) => {
|
export const createContainer = (params: Container.ContainerCreate) => {
|
||||||
@ -94,17 +94,28 @@ export const deleteImageRepo = (params: { ids: number[] }) => {
|
|||||||
|
|
||||||
// composeTemplate
|
// composeTemplate
|
||||||
export const searchComposeTemplate = (params: ReqPage) => {
|
export const searchComposeTemplate = (params: ReqPage) => {
|
||||||
return http.post<ResPage<Container.TemplateInfo>>(`/containers/compose/search`, params);
|
return http.post<ResPage<Container.TemplateInfo>>(`/containers/template/search`, params);
|
||||||
};
|
};
|
||||||
export const listComposeTemplate = (params: ReqPage) => {
|
export const listComposeTemplate = () => {
|
||||||
return http.post<ResPage<Container.TemplateInfo>>(`/containers/compose/search`, params);
|
return http.get<Container.TemplateInfo>(`/containers/template`);
|
||||||
};
|
};
|
||||||
export const deleteComposeTemplate = (params: { ids: number[] }) => {
|
export const deleteComposeTemplate = (params: { ids: number[] }) => {
|
||||||
return http.post(`/containers/compose/del`, params);
|
return http.post(`/containers/template/del`, params);
|
||||||
};
|
};
|
||||||
export const createComposeTemplate = (params: Container.TemplateCreate) => {
|
export const createComposeTemplate = (params: Container.TemplateCreate) => {
|
||||||
return http.post(`/containers/compose`, params);
|
return http.post(`/containers/template`, params);
|
||||||
};
|
};
|
||||||
export const updateComposeTemplate = (params: Container.TemplateUpdate) => {
|
export const updateComposeTemplate = (params: Container.TemplateUpdate) => {
|
||||||
return http.put(`/containers/compose/${params.id}`, params);
|
return http.put(`/containers/template/${params.id}`, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
// compose
|
||||||
|
export const searchCompose = (params: ReqPage) => {
|
||||||
|
return http.post<ResPage<Container.ComposeInfo>>(`/containers/compose/search`, params);
|
||||||
|
};
|
||||||
|
export const upCompose = (params: Container.ComposeCreate) => {
|
||||||
|
return http.post(`/containers/compose/up`, params);
|
||||||
|
};
|
||||||
|
export const ComposeOperator = (params: Container.ComposeOpration) => {
|
||||||
|
return http.post(`/containers/compose/operate`, params);
|
||||||
};
|
};
|
||||||
|
@ -155,11 +155,11 @@ export default {
|
|||||||
operatorHelper: '{0} will be performed on the selected container. Do you want to continue?',
|
operatorHelper: '{0} will be performed on the selected container. Do you want to continue?',
|
||||||
start: 'Start',
|
start: 'Start',
|
||||||
stop: 'Stop',
|
stop: 'Stop',
|
||||||
reStart: 'ReStart',
|
reStart: 'Restart',
|
||||||
kill: 'Kill',
|
kill: 'Kill',
|
||||||
pause: 'Pause',
|
pause: 'Pause',
|
||||||
unPause: 'UnPause',
|
unpause: 'Unpause',
|
||||||
reName: 'ReName',
|
rename: 'Rename',
|
||||||
remove: 'Remove',
|
remove: 'Remove',
|
||||||
container: 'Container',
|
container: 'Container',
|
||||||
upTime: 'UpTime',
|
upTime: 'UpTime',
|
||||||
@ -247,6 +247,10 @@ export default {
|
|||||||
composeTemplate: 'Compose template',
|
composeTemplate: 'Compose template',
|
||||||
description: 'Description',
|
description: 'Description',
|
||||||
content: 'Content',
|
content: 'Content',
|
||||||
|
containerNumber: 'Container number',
|
||||||
|
down: 'Down',
|
||||||
|
up: 'Up',
|
||||||
|
operatorComposeHelper: '{0} will be performed on the selected compose. Do you want to continue?',
|
||||||
},
|
},
|
||||||
cronjob: {
|
cronjob: {
|
||||||
cronTask: 'Task',
|
cronTask: 'Task',
|
||||||
|
@ -153,11 +153,11 @@ export default {
|
|||||||
operatorHelper: '将对选中容器进行 {0} 操作,是否继续?',
|
operatorHelper: '将对选中容器进行 {0} 操作,是否继续?',
|
||||||
start: '启动',
|
start: '启动',
|
||||||
stop: '停止',
|
stop: '停止',
|
||||||
reStart: '重启',
|
restart: '重启',
|
||||||
kill: '强制停止',
|
kill: '强制停止',
|
||||||
pause: '暂停',
|
pause: '暂停',
|
||||||
unPause: '恢复',
|
unpause: '恢复',
|
||||||
reName: '重命名',
|
rename: '重命名',
|
||||||
remove: '移除',
|
remove: '移除',
|
||||||
container: '容器',
|
container: '容器',
|
||||||
upTime: '运行时长',
|
upTime: '运行时长',
|
||||||
@ -248,6 +248,10 @@ export default {
|
|||||||
composeTemplate: '编排模版',
|
composeTemplate: '编排模版',
|
||||||
description: '描述',
|
description: '描述',
|
||||||
content: '内容',
|
content: '内容',
|
||||||
|
containerNumber: '容器数量',
|
||||||
|
down: '删除',
|
||||||
|
up: '启动',
|
||||||
|
operatorComposeHelper: '将对选中 Compose 进行 {0} 操作,是否继续?',
|
||||||
},
|
},
|
||||||
cronjob: {
|
cronjob: {
|
||||||
cronTask: '计划任务',
|
cronTask: '计划任务',
|
||||||
|
@ -11,10 +11,74 @@ const containerRouter = {
|
|||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '/containers',
|
path: ':filters?',
|
||||||
name: 'Container',
|
name: 'Container',
|
||||||
component: () => import('@/views/container/index.vue'),
|
component: () => import('@/views/container/container/index.vue'),
|
||||||
meta: {},
|
props: true,
|
||||||
|
hidden: true,
|
||||||
|
meta: {
|
||||||
|
activeMenu: '/containers',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'image',
|
||||||
|
name: 'Image',
|
||||||
|
component: () => import('@/views/container/image/index.vue'),
|
||||||
|
props: true,
|
||||||
|
hidden: true,
|
||||||
|
meta: {
|
||||||
|
activeMenu: '/containers',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'network',
|
||||||
|
name: 'Network',
|
||||||
|
component: () => import('@/views/container/network/index.vue'),
|
||||||
|
props: true,
|
||||||
|
hidden: true,
|
||||||
|
meta: {
|
||||||
|
activeMenu: '/containers',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'volume',
|
||||||
|
name: 'Volume',
|
||||||
|
component: () => import('@/views/container/volume/index.vue'),
|
||||||
|
props: true,
|
||||||
|
hidden: true,
|
||||||
|
meta: {
|
||||||
|
activeMenu: '/containers',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'repo',
|
||||||
|
name: 'Repo',
|
||||||
|
component: () => import('@/views/container/repo/index.vue'),
|
||||||
|
props: true,
|
||||||
|
hidden: true,
|
||||||
|
meta: {
|
||||||
|
activeMenu: '/containers',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'compose',
|
||||||
|
name: 'Compose',
|
||||||
|
component: () => import('@/views/container/compose/index.vue'),
|
||||||
|
props: true,
|
||||||
|
hidden: true,
|
||||||
|
meta: {
|
||||||
|
activeMenu: '/containers',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'template',
|
||||||
|
name: 'composeTemplate',
|
||||||
|
component: () => import('@/views/container/template/index.vue'),
|
||||||
|
props: true,
|
||||||
|
hidden: true,
|
||||||
|
meta: {
|
||||||
|
activeMenu: '/containers',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
140
frontend/src/views/container/compose/index.vue
Normal file
140
frontend/src/views/container/compose/index.vue
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Submenu activeName="compose" />
|
||||||
|
<el-card style="margin-top: 20px">
|
||||||
|
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
||||||
|
<template #toolbar>
|
||||||
|
<el-button-group>
|
||||||
|
<el-button :disabled="selects.length === 0" @click="onOperate('up')">
|
||||||
|
{{ $t('container.start') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button :disabled="selects.length === 0" @click="onOperate('stop')">
|
||||||
|
{{ $t('container.stop') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button :disabled="selects.length === 0" @click="onOperate('pause')">
|
||||||
|
{{ $t('container.pause') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button :disabled="selects.length === 0" @click="onOperate('unpause')">
|
||||||
|
{{ $t('container.unpause') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button :disabled="selects.length === 0" @click="onOperate('restart')">
|
||||||
|
{{ $t('container.restart') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button :disabled="selects.length === 0" @click="onOperate('down')">
|
||||||
|
{{ $t('container.down') }}
|
||||||
|
</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
<el-button icon="Plus" style="margin-left: 10px" @click="onOpenDialog()">
|
||||||
|
{{ $t('commons.button.create') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<el-table-column type="selection" fix></el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
:label="$t('commons.table.name')"
|
||||||
|
show-overflow-tooltip
|
||||||
|
min-width="100"
|
||||||
|
prop="name"
|
||||||
|
fix
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link @click="goContainer(row.name)" type="primary">{{ row.name }}</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column :label="$t('container.containerNumber')" prop="containerNumber" min-width="80" fix />
|
||||||
|
<el-table-column :label="$t('container.containerNumber')" prop="contaienrs" min-width="80" fix>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-for="(item, index) in row.containers" :key="index">
|
||||||
|
<div v-if="row.expand || (!row.expand && index < 1)">
|
||||||
|
<el-tag>{{ item.name }} [{{ item.state }}]</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!row.expand">
|
||||||
|
<el-button type="primary" link @click="row.expand = true">
|
||||||
|
{{ $t('commons.button.expand') }}...
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column :label="$t('commons.table.createdAt')" prop="createdAt" min-width="80" fix />
|
||||||
|
</ComplexTable>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<OperatorDialog @search="search" ref="dialogRef" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
|
import { reactive, onMounted, ref } from 'vue';
|
||||||
|
import OperatorDialog from '@/views/container/compose/operator/index.vue';
|
||||||
|
import Submenu from '@/views/container/index.vue';
|
||||||
|
import { ComposeOperator, searchCompose } from '@/api/modules/container';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import router from '@/routers';
|
||||||
|
|
||||||
|
const data = ref();
|
||||||
|
const selects = ref<any>([]);
|
||||||
|
|
||||||
|
const paginationConfig = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
let params = {
|
||||||
|
page: paginationConfig.page,
|
||||||
|
pageSize: paginationConfig.pageSize,
|
||||||
|
};
|
||||||
|
await searchCompose(params).then((res) => {
|
||||||
|
if (res.data) {
|
||||||
|
data.value = res.data.items;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const goContainer = async (name: string) => {
|
||||||
|
router.push({ name: 'Container', params: { filters: 'com.docker.compose.project=' + name } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialogRef = ref();
|
||||||
|
const onOpenDialog = async () => {
|
||||||
|
dialogRef.value!.acceptParams();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOperate = async (operation: string) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
i18n.global.t('container.operatorComposeHelper', [i18n.global.t('container.' + operation)]),
|
||||||
|
i18n.global.t('container.' + operation),
|
||||||
|
{
|
||||||
|
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||||
|
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||||
|
type: 'info',
|
||||||
|
},
|
||||||
|
).then(() => {
|
||||||
|
let ps = [];
|
||||||
|
for (const item of selects.value) {
|
||||||
|
const param = {
|
||||||
|
path: item.path,
|
||||||
|
operation: operation,
|
||||||
|
};
|
||||||
|
ps.push(ComposeOperator(param));
|
||||||
|
}
|
||||||
|
Promise.all(ps)
|
||||||
|
.then(() => {
|
||||||
|
search();
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// const buttons = [];
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
</script>
|
133
frontend/src/views/container/compose/operator/index.vue
Normal file
133
frontend/src/views/container/compose/operator/index.vue
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="composeVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="50%">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>{{ $t('container.compose') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||||
|
<el-form-item :label="$t('container.name')" prop="name">
|
||||||
|
<el-input v-model="form.name"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('container.from')">
|
||||||
|
<el-radio-group v-model="form.from">
|
||||||
|
<el-radio label="edit">{{ $t('container.edit') }}</el-radio>
|
||||||
|
<el-radio label="path">{{ $t('container.pathSelect') }}</el-radio>
|
||||||
|
<el-radio label="template">{{ $t('container.composeTemplate') }}</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="form.from === 'path'" prop="path">
|
||||||
|
<el-input
|
||||||
|
clearable
|
||||||
|
:placeholder="$t('commons.example') + '/tmp/docker-compose.yml'"
|
||||||
|
v-model="form.path"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<FileList @choose="loadDir" :dir="false"></FileList>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="form.from === 'template'" prop="template">
|
||||||
|
<el-select v-model="form.template">
|
||||||
|
<el-option v-for="item in templateOptions" :key="item.id" :value="item.id" :label="item.name" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="form.from === 'edit'">
|
||||||
|
<codemirror
|
||||||
|
:autofocus="true"
|
||||||
|
placeholder="None data"
|
||||||
|
:indent-with-tab="true"
|
||||||
|
:tabSize="4"
|
||||||
|
style="max-height: 500px; width: 100%"
|
||||||
|
:lineWrapping="true"
|
||||||
|
:matchBrackets="true"
|
||||||
|
theme="cobalt"
|
||||||
|
:styleActiveLine="true"
|
||||||
|
:extensions="extensions"
|
||||||
|
v-model="form.file"
|
||||||
|
:readOnly="true"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="composeVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
<el-button type="primary" @click="onSubmit(formRef)">
|
||||||
|
{{ $t('commons.button.confirm') }}
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import FileList from '@/components/file-list/index.vue';
|
||||||
|
import { Codemirror } from 'vue-codemirror';
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
|
import { Rules } from '@/global/form-rules';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { ElForm, ElMessage } from 'element-plus';
|
||||||
|
import { listComposeTemplate, upCompose } from '@/api/modules/container';
|
||||||
|
|
||||||
|
const extensions = [javascript(), oneDark];
|
||||||
|
const composeVisiable = ref(false);
|
||||||
|
const templateOptions = ref();
|
||||||
|
|
||||||
|
const varifyPath = (rule: any, value: any, callback: any) => {
|
||||||
|
if (value.indexOf('docker-compose.yml') === -1) {
|
||||||
|
callback(new Error(i18n.global.t('commons.rule.selectHelper', ['docker-compose.yml'])));
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
const form = reactive({
|
||||||
|
name: '',
|
||||||
|
from: 'edit',
|
||||||
|
path: '',
|
||||||
|
file: '',
|
||||||
|
template: 0,
|
||||||
|
});
|
||||||
|
const rules = reactive({
|
||||||
|
name: [Rules.requiredInput, Rules.name],
|
||||||
|
path: [Rules.requiredSelect, { validator: varifyPath, trigger: 'change', required: true }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadTemplates = async () => {
|
||||||
|
const res = await listComposeTemplate();
|
||||||
|
templateOptions.value = res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const acceptParams = (): void => {
|
||||||
|
composeVisiable.value = true;
|
||||||
|
form.name = '';
|
||||||
|
form.from = 'edit';
|
||||||
|
form.path = '';
|
||||||
|
form.file = '';
|
||||||
|
form.template = 0;
|
||||||
|
loadTemplates();
|
||||||
|
};
|
||||||
|
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||||
|
|
||||||
|
type FormInstance = InstanceType<typeof ElForm>;
|
||||||
|
const formRef = ref<FormInstance>();
|
||||||
|
|
||||||
|
const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.validate(async (valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
await upCompose(form);
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
emit('search');
|
||||||
|
composeVisiable.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadDir = async (path: string) => {
|
||||||
|
form.path = path;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
acceptParams,
|
||||||
|
});
|
||||||
|
</script>
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<Submenu activeName="container" />
|
||||||
<el-card style="margin-top: 20px">
|
<el-card style="margin-top: 20px">
|
||||||
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
@ -10,8 +11,8 @@
|
|||||||
<el-button :disabled="checkStatus('stop')" @click="onOperate('stop')">
|
<el-button :disabled="checkStatus('stop')" @click="onOperate('stop')">
|
||||||
{{ $t('container.stop') }}
|
{{ $t('container.stop') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button :disabled="checkStatus('reStart')" @click="onOperate('reStart')">
|
<el-button :disabled="checkStatus('restart')" @click="onOperate('restart')">
|
||||||
{{ $t('container.reStart') }}
|
{{ $t('container.restart') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button :disabled="checkStatus('kill')" @click="onOperate('kill')">
|
<el-button :disabled="checkStatus('kill')" @click="onOperate('kill')">
|
||||||
{{ $t('container.kill') }}
|
{{ $t('container.kill') }}
|
||||||
@ -19,8 +20,8 @@
|
|||||||
<el-button :disabled="checkStatus('pause')" @click="onOperate('pause')">
|
<el-button :disabled="checkStatus('pause')" @click="onOperate('pause')">
|
||||||
{{ $t('container.pause') }}
|
{{ $t('container.pause') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button :disabled="checkStatus('unPause')" @click="onOperate('unPause')">
|
<el-button :disabled="checkStatus('unpause')" @click="onOperate('unpause')">
|
||||||
{{ $t('container.unPause') }}
|
{{ $t('container.unpause') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button :disabled="checkStatus('remove')" @click="onOperate('remove')">
|
<el-button :disabled="checkStatus('remove')" @click="onOperate('remove')">
|
||||||
{{ $t('container.remove') }}
|
{{ $t('container.remove') }}
|
||||||
@ -141,12 +142,12 @@
|
|||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>{{ $t('container.reName') }}</span>
|
<span>{{ $t('container.rename') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-form ref="newNameRef" :model="reNameForm">
|
<el-form ref="newNameRef" :model="renameForm">
|
||||||
<el-form-item label="新名称" :rules="Rules.requiredInput" prop="newName">
|
<el-form-item label="新名称" :rules="Rules.requiredInput" prop="newName">
|
||||||
<el-input v-model="reNameForm.newName"></el-input>
|
<el-input v-model="renameForm.newName"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -165,6 +166,7 @@
|
|||||||
import ComplexTable from '@/components/complex-table/index.vue';
|
import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
import CreateDialog from '@/views/container/container/create/index.vue';
|
import CreateDialog from '@/views/container/container/create/index.vue';
|
||||||
import MonitorDialog from '@/views/container/container/monitor/index.vue';
|
import MonitorDialog from '@/views/container/container/monitor/index.vue';
|
||||||
|
import Submenu from '@/views/container/index.vue';
|
||||||
import { Codemirror } from 'vue-codemirror';
|
import { Codemirror } from 'vue-codemirror';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
@ -183,10 +185,12 @@ const paginationConfig = reactive({
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
const containerSearch = reactive({
|
|
||||||
page: 1,
|
interface Filters {
|
||||||
pageSize: 5,
|
filters?: string;
|
||||||
status: 'all',
|
}
|
||||||
|
const props = withDefaults(defineProps<Filters>(), {
|
||||||
|
filters: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const detailVisiable = ref<boolean>(false);
|
const detailVisiable = ref<boolean>(false);
|
||||||
@ -205,9 +209,9 @@ let timer: NodeJS.Timer | null = null;
|
|||||||
const newNameVisiable = ref<boolean>(false);
|
const newNameVisiable = ref<boolean>(false);
|
||||||
type FormInstance = InstanceType<typeof ElForm>;
|
type FormInstance = InstanceType<typeof ElForm>;
|
||||||
const newNameRef = ref<FormInstance>();
|
const newNameRef = ref<FormInstance>();
|
||||||
const reNameForm = reactive({
|
const renameForm = reactive({
|
||||||
containerID: '',
|
containerID: '',
|
||||||
operation: 'reName',
|
operation: 'rename',
|
||||||
newName: '',
|
newName: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -232,9 +236,13 @@ const timeOptions = ref([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const search = async () => {
|
const search = async () => {
|
||||||
containerSearch.page = paginationConfig.page;
|
let filterItem = props.filters ? props.filters : '';
|
||||||
containerSearch.pageSize = paginationConfig.pageSize;
|
let params = {
|
||||||
await searchContainer(containerSearch).then((res) => {
|
page: paginationConfig.page,
|
||||||
|
pageSize: paginationConfig.pageSize,
|
||||||
|
filters: filterItem,
|
||||||
|
};
|
||||||
|
await searchContainer(params).then((res) => {
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
data.value = res.data.items;
|
data.value = res.data.items;
|
||||||
}
|
}
|
||||||
@ -286,15 +294,15 @@ const onDownload = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onRename = async (row: Container.ContainerInfo) => {
|
const onRename = async (row: Container.ContainerInfo) => {
|
||||||
reNameForm.containerID = row.containerID;
|
renameForm.containerID = row.containerID;
|
||||||
reNameForm.newName = '';
|
renameForm.newName = '';
|
||||||
newNameVisiable.value = true;
|
newNameVisiable.value = true;
|
||||||
};
|
};
|
||||||
const onSubmitName = async (formEl: FormInstance | undefined) => {
|
const onSubmitName = async (formEl: FormInstance | undefined) => {
|
||||||
if (!formEl) return;
|
if (!formEl) return;
|
||||||
formEl.validate(async (valid) => {
|
formEl.validate(async (valid) => {
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
ContainerOperator(reNameForm);
|
ContainerOperator(renameForm);
|
||||||
search();
|
search();
|
||||||
newNameVisiable.value = false;
|
newNameVisiable.value = false;
|
||||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
@ -327,7 +335,7 @@ const checkStatus = (operation: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
case 'unPause':
|
case 'unpause':
|
||||||
for (const item of selects.value) {
|
for (const item of selects.value) {
|
||||||
if (item.state !== 'paused') {
|
if (item.state !== 'paused') {
|
||||||
return true;
|
return true;
|
||||||
@ -338,7 +346,7 @@ const checkStatus = (operation: string) => {
|
|||||||
};
|
};
|
||||||
const onOperate = async (operation: string) => {
|
const onOperate = async (operation: string) => {
|
||||||
ElMessageBox.confirm(
|
ElMessageBox.confirm(
|
||||||
i18n.global.t('container.operatorHelper', [operation]),
|
i18n.global.t('container.operatorHelper', [i18n.global.t('container.' + operation)]),
|
||||||
i18n.global.t('container.' + operation),
|
i18n.global.t('container.' + operation),
|
||||||
{
|
{
|
||||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||||
@ -374,7 +382,7 @@ const buttons = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.global.t('container.reName'),
|
label: i18n.global.t('container.rename'),
|
||||||
click: (row: Container.ContainerInfo) => {
|
click: (row: Container.ContainerInfo) => {
|
||||||
onRename(row);
|
onRename(row);
|
||||||
},
|
},
|
||||||
|
@ -8,20 +8,20 @@
|
|||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>{{ $t('container.buildImage') }}</span>
|
<span>{{ $t('container.imageBuild') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-form ref="formRef" :model="form" label-width="80px">
|
<el-form ref="formRef" :model="form" label-width="80px" :rules="rules">
|
||||||
<el-form-item :label="$t('container.name')" :rules="Rules.requiredInput" prop="name">
|
<el-form-item :label="$t('container.name')" prop="name">
|
||||||
<el-input :placeholder="$t('container.imageNameHelper')" v-model="form.name" clearable />
|
<el-input :placeholder="$t('container.imageNameHelper')" v-model="form.name" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Dockerfile" :rules="Rules.requiredSelect" prop="from">
|
<el-form-item label="Dockerfile" prop="from">
|
||||||
<el-radio-group v-model="form.from">
|
<el-radio-group v-model="form.from">
|
||||||
<el-radio label="edit">{{ $t('container.edit') }}</el-radio>
|
<el-radio label="edit">{{ $t('container.edit') }}</el-radio>
|
||||||
<el-radio label="path">{{ $t('container.pathSelect') }}</el-radio>
|
<el-radio label="path">{{ $t('container.pathSelect') }}</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="form.from === 'edit'" :rules="Rules.requiredInput" prop="dockerfile">
|
<el-form-item v-if="form.from === 'edit'" :rules="Rules.requiredInput">
|
||||||
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 10 }" v-model="form.dockerfile" />
|
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 10 }" v-model="form.dockerfile" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-else :rules="Rules.requiredSelect" prop="dockerfile">
|
<el-form-item v-else :rules="Rules.requiredSelect" prop="dockerfile">
|
||||||
@ -101,7 +101,8 @@ const varifyPath = (rule: any, value: any, callback: any) => {
|
|||||||
};
|
};
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
name: [Rules.requiredInput, Rules.name],
|
name: [Rules.requiredInput, Rules.name],
|
||||||
content: [Rules.requiredInput, { validator: varifyPath, trigger: 'change', required: true }],
|
from: [Rules.requiredSelect],
|
||||||
|
dockerfile: [Rules.requiredInput, { validator: varifyPath, trigger: 'change', required: true }],
|
||||||
});
|
});
|
||||||
const acceptParams = async () => {
|
const acceptParams = async () => {
|
||||||
buildVisiable.value = true;
|
buildVisiable.value = true;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-loading="loading">
|
<div v-loading="loading">
|
||||||
|
<Submenu activeName="image" />
|
||||||
<el-card style="margin-top: 20px">
|
<el-card style="margin-top: 20px">
|
||||||
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
@ -71,6 +72,7 @@
|
|||||||
import ComplexTable from '@/components/complex-table/index.vue';
|
import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
import { reactive, onMounted, ref } from 'vue';
|
import { reactive, onMounted, ref } from 'vue';
|
||||||
import { dateFromat } from '@/utils/util';
|
import { dateFromat } from '@/utils/util';
|
||||||
|
import Submenu from '@/views/container/index.vue';
|
||||||
import { Container } from '@/api/interface/container';
|
import { Container } from '@/api/interface/container';
|
||||||
import Pull from '@/views/container/image/pull/index.vue';
|
import Pull from '@/views/container/image/pull/index.vue';
|
||||||
import Tag from '@/views/container/image/tag/index.vue';
|
import Tag from '@/views/container/image/tag/index.vue';
|
||||||
|
@ -1,49 +1,70 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-card class="topCard">
|
<el-card class="topCard">
|
||||||
<el-radio-group v-model="activeNames">
|
<el-radio-group v-model="active">
|
||||||
<el-radio-button class="topButton" size="large" label="container">
|
<el-radio-button class="topButton" size="large" @click="routerTo('/containers')" label="container">
|
||||||
{{ $t('container.container') }}
|
{{ $t('container.container') }}
|
||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
<el-radio-button class="topButton" size="large" label="image">
|
<el-radio-button class="topButton" size="large" @click="routerTo('/containers/image')" label="image">
|
||||||
{{ $t('container.image') }}
|
{{ $t('container.image') }}
|
||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
<el-radio-button class="topButton" size="large" label="network">
|
<el-radio-button
|
||||||
|
class="topButton"
|
||||||
|
size="large"
|
||||||
|
@click="routerTo('/containers/network')"
|
||||||
|
label="network"
|
||||||
|
>
|
||||||
{{ $t('container.network') }}
|
{{ $t('container.network') }}
|
||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
<el-radio-button class="topButton" size="large" label="volume">
|
<el-radio-button class="topButton" size="large" @click="routerTo('/containers/volume')" label="volume">
|
||||||
{{ $t('container.volume') }}
|
{{ $t('container.volume') }}
|
||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
<el-radio-button class="topButton" size="large" label="repo">
|
<el-radio-button class="topButton" size="large" @click="routerTo('/containers/repo')" label="repo">
|
||||||
{{ $t('container.repo') }}
|
{{ $t('container.repo') }}
|
||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
<el-radio-button class="topButton" size="large" label="compose">
|
<el-radio-button
|
||||||
|
class="topButton"
|
||||||
|
size="large"
|
||||||
|
@click="routerTo('/containers/compose')"
|
||||||
|
label="compose"
|
||||||
|
>
|
||||||
{{ $t('container.compose') }}
|
{{ $t('container.compose') }}
|
||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
<el-radio-button class="topButton" size="large" label="composeTemplate">
|
<el-radio-button
|
||||||
|
class="topButton"
|
||||||
|
size="large"
|
||||||
|
@click="routerTo('/containers/template')"
|
||||||
|
label="template"
|
||||||
|
>
|
||||||
{{ $t('container.composeTemplate') }}
|
{{ $t('container.composeTemplate') }}
|
||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-card>
|
</el-card>
|
||||||
<Container v-if="activeNames === 'container'" />
|
|
||||||
<Repo v-if="activeNames === 'repo'" />
|
|
||||||
<Image v-if="activeNames === 'image'" />
|
|
||||||
<Network v-if="activeNames === 'network'" />
|
|
||||||
<Volume v-if="activeNames === 'volume'" />
|
|
||||||
<ComposeTemplate v-if="activeNames === 'composeTemplate'" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import Container from '@/views/container/container/index.vue';
|
import { useRouter } from 'vue-router';
|
||||||
import Repo from '@/views/container/repo/index.vue';
|
const router = useRouter();
|
||||||
import Image from '@/views/container/image/index.vue';
|
interface MenuProps {
|
||||||
import Network from '@/views/container/network/index.vue';
|
activeName: string;
|
||||||
import Volume from '@/views/container/volume/index.vue';
|
}
|
||||||
import ComposeTemplate from '@/views/container/compose/template/index.vue';
|
const props = withDefaults(defineProps<MenuProps>(), {
|
||||||
|
activeName: 'container',
|
||||||
|
});
|
||||||
|
|
||||||
const activeNames = ref('container');
|
const active = ref();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.activeName) {
|
||||||
|
active.value = props.activeName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const routerTo = (path: string) => {
|
||||||
|
router.push({ path: path });
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<Submenu activeName="network" />
|
||||||
<el-card style="margin-top: 20px">
|
<el-card style="margin-top: 20px">
|
||||||
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
@ -78,6 +79,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import ComplexTable from '@/components/complex-table/index.vue';
|
import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
import CreateDialog from '@/views/container/network/create/index.vue';
|
import CreateDialog from '@/views/container/network/create/index.vue';
|
||||||
|
import Submenu from '@/views/container/index.vue';
|
||||||
import { Codemirror } from 'vue-codemirror';
|
import { Codemirror } from 'vue-codemirror';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<Submenu activeName="repo" />
|
||||||
<el-card style="margin-top: 20px">
|
<el-card style="margin-top: 20px">
|
||||||
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
@ -35,6 +36,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import ComplexTable from '@/components/complex-table/index.vue';
|
import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
import OperatorDialog from '@/views/container/repo/operator/index.vue';
|
import OperatorDialog from '@/views/container/repo/operator/index.vue';
|
||||||
|
import Submenu from '@/views/container/index.vue';
|
||||||
import { reactive, onMounted, ref } from 'vue';
|
import { reactive, onMounted, ref } from 'vue';
|
||||||
import { dateFromat } from '@/utils/util';
|
import { dateFromat } from '@/utils/util';
|
||||||
import { Container } from '@/api/interface/container';
|
import { Container } from '@/api/interface/container';
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<Submenu activeName="template" />
|
||||||
<el-card style="margin-top: 20px">
|
<el-card style="margin-top: 20px">
|
||||||
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
@ -65,13 +66,14 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import ComplexTable from '@/components/complex-table/index.vue';
|
import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
|
import Submenu from '@/views/container/index.vue';
|
||||||
import { Codemirror } from 'vue-codemirror';
|
import { Codemirror } from 'vue-codemirror';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
import { reactive, onMounted, ref } from 'vue';
|
import { reactive, onMounted, ref } from 'vue';
|
||||||
import { dateFromat } from '@/utils/util';
|
import { dateFromat } from '@/utils/util';
|
||||||
import { Container } from '@/api/interface/container';
|
import { Container } from '@/api/interface/container';
|
||||||
import OperatorDialog from '@/views/container/compose/template/operator/index.vue';
|
import OperatorDialog from '@/views/container/template/operator/index.vue';
|
||||||
import { deleteComposeTemplate, searchComposeTemplate } from '@/api/modules/container';
|
import { deleteComposeTemplate, searchComposeTemplate } from '@/api/modules/container';
|
||||||
import { useDeleteData } from '@/hooks/use-delete-data';
|
import { useDeleteData } from '@/hooks/use-delete-data';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
@ -106,7 +108,7 @@ const onOpenDetail = async (row: Container.TemplateInfo) => {
|
|||||||
detailInfo.value = row.content;
|
detailInfo.value = row.content;
|
||||||
detailVisiable.value = true;
|
detailVisiable.value = true;
|
||||||
} else {
|
} else {
|
||||||
const res = await LoadFile({ path: row.content });
|
const res = await LoadFile({ path: row.path });
|
||||||
detailInfo.value = res.data;
|
detailInfo.value = res.data;
|
||||||
detailVisiable.value = true;
|
detailVisiable.value = true;
|
||||||
}
|
}
|
||||||
@ -119,6 +121,7 @@ const onOpenDialog = async (
|
|||||||
name: '',
|
name: '',
|
||||||
from: 'edit',
|
from: 'edit',
|
||||||
description: '',
|
description: '',
|
||||||
|
path: '',
|
||||||
content: '',
|
content: '',
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
@ -18,11 +18,11 @@
|
|||||||
<el-radio label="path">{{ $t('container.pathSelect') }}</el-radio>
|
<el-radio label="path">{{ $t('container.pathSelect') }}</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="dialogData.rowData!.from === 'path'" :rules="Rules.requiredSelect" prop="content">
|
<el-form-item v-if="dialogData.rowData!.from === 'path'" prop="path">
|
||||||
<el-input
|
<el-input
|
||||||
clearable
|
clearable
|
||||||
:placeholder="$t('commons.example') + '/tmp/docker-compose.yml'"
|
:placeholder="$t('commons.example') + '/tmp/docker-compose.yml'"
|
||||||
v-model="dialogData.rowData!.content"
|
v-model="dialogData.rowData!.path"
|
||||||
>
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
<FileList @choose="loadDir" :dir="false"></FileList>
|
<FileList @choose="loadDir" :dir="false"></FileList>
|
||||||
@ -88,14 +88,16 @@ const acceptParams = (params: DialogProps): void => {
|
|||||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||||
|
|
||||||
const varifyPath = (rule: any, value: any, callback: any) => {
|
const varifyPath = (rule: any, value: any, callback: any) => {
|
||||||
|
console.log(value, value.indexOf('docker-compose.yml'));
|
||||||
if (value.indexOf('docker-compose.yml') === -1) {
|
if (value.indexOf('docker-compose.yml') === -1) {
|
||||||
callback(new Error(i18n.global.t('commons.rule.selectHelper', ['.docker-compose.yml'])));
|
callback(new Error(i18n.global.t('commons.rule.selectHelper', ['docker-compose.yml'])));
|
||||||
}
|
}
|
||||||
callback();
|
callback();
|
||||||
};
|
};
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
name: [Rules.requiredInput, Rules.name],
|
name: [Rules.requiredInput, Rules.name],
|
||||||
content: [Rules.requiredInput, { validator: varifyPath, trigger: 'change', required: true }],
|
path: [Rules.requiredInput, { validator: varifyPath, trigger: 'change', required: true }],
|
||||||
|
content: [Rules.requiredInput],
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormInstance = InstanceType<typeof ElForm>;
|
type FormInstance = InstanceType<typeof ElForm>;
|
||||||
@ -124,7 +126,7 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadDir = async (path: string) => {
|
const loadDir = async (path: string) => {
|
||||||
dialogData.value.rowData!.content = path;
|
dialogData.value.rowData!.path = path;
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<Submenu activeName="volume" />
|
||||||
<el-card style="margin-top: 20px">
|
<el-card style="margin-top: 20px">
|
||||||
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
@ -67,6 +68,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import ComplexTable from '@/components/complex-table/index.vue';
|
import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
import CreateDialog from '@/views/container/volume/create/index.vue';
|
import CreateDialog from '@/views/container/volume/create/index.vue';
|
||||||
|
import Submenu from '@/views/container/index.vue';
|
||||||
import { Codemirror } from 'vue-codemirror';
|
import { Codemirror } from 'vue-codemirror';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
|
@ -96,14 +96,14 @@ const loginRules = reactive({
|
|||||||
|
|
||||||
const loginForm = reactive<Login.ReqLoginForm>({
|
const loginForm = reactive<Login.ReqLoginForm>({
|
||||||
name: 'admin',
|
name: 'admin',
|
||||||
password: 'Songliu123++',
|
password: 'Calong@2015',
|
||||||
captcha: '',
|
captcha: '',
|
||||||
captchaID: '',
|
captchaID: '',
|
||||||
authMethod: '',
|
authMethod: '',
|
||||||
});
|
});
|
||||||
const mfaLoginForm = reactive<Login.MFALoginForm>({
|
const mfaLoginForm = reactive<Login.MFALoginForm>({
|
||||||
name: 'admin',
|
name: 'admin',
|
||||||
password: 'Songliu123++',
|
password: 'Calong@2015',
|
||||||
secret: '',
|
secret: '',
|
||||||
code: '',
|
code: '',
|
||||||
authMethod: '',
|
authMethod: '',
|
||||||
|
2
go.mod
2
go.mod
@ -148,7 +148,5 @@ require (
|
|||||||
golang.org/x/tools v0.1.12 // indirect
|
golang.org/x/tools v0.1.12 // indirect
|
||||||
google.golang.org/protobuf v1.28.0 // indirect
|
google.golang.org/protobuf v1.28.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.66.6 // indirect
|
gopkg.in/ini.v1 v1.66.6 // indirect
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
gotest.tools/v3 v3.3.0 // indirect
|
gotest.tools/v3 v3.3.0 // indirect
|
||||||
)
|
)
|
||||||
|
1
go.sum
1
go.sum
@ -591,6 +591,7 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw
|
|||||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||||
github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
|
github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
|
||||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
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/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
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/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
Loading…
x
Reference in New Issue
Block a user