mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-31 14:08:06 +08:00
feat: 增加镜像 tag 功能
This commit is contained in:
parent
1d2a00cc6e
commit
39f9de0b00
@ -89,7 +89,7 @@ func (b *BaseApi) ImagePush(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *BaseApi) ImageRemove(c *gin.Context) {
|
func (b *BaseApi) ImageRemove(c *gin.Context) {
|
||||||
var req dto.ImageRemove
|
var req dto.BatchDelete
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
return
|
return
|
||||||
@ -126,6 +126,25 @@ func (b *BaseApi) ImageSave(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, nil)
|
helper.SuccessWithData(c, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) ImageTag(c *gin.Context) {
|
||||||
|
var req dto.ImageTag
|
||||||
|
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 := imageService.ImageTag(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BaseApi) ImageLoad(c *gin.Context) {
|
func (b *BaseApi) ImageLoad(c *gin.Context) {
|
||||||
var req dto.ImageLoad
|
var req dto.ImageLoad
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
@ -29,26 +29,24 @@ type ContainerOperation struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Network struct {
|
type Network struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Labels []string `json:"labels"`
|
Labels []string `json:"labels"`
|
||||||
Driver string `json:"driver"`
|
Driver string `json:"driver"`
|
||||||
IPAMDriver string `json:"ipamDriver"`
|
IPAMDriver string `json:"ipamDriver"`
|
||||||
IPV4Subnet string `json:"ipv4Subnet"`
|
Subnet string `json:"subnet"`
|
||||||
IPV4Gateway string `json:"ipv4Gateway"`
|
Gateway string `json:"gateway"`
|
||||||
IPV6Subnet string `json:"ipv6Subnet"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
IPV6Gateway string `json:"ipv6Gateway"`
|
Attachable bool `json:"attachable"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
|
||||||
Attachable bool `json:"attachable"`
|
|
||||||
}
|
}
|
||||||
type NetworkCreat struct {
|
type NetworkCreat struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Driver string `json:"driver"`
|
Driver string `json:"driver"`
|
||||||
Options []string `json:"options"`
|
Options []string `json:"options"`
|
||||||
IPV4Subnet string `json:"ipv4Subnet"`
|
Subnet string `json:"subnet"`
|
||||||
IPV4Gateway string `json:"ipv4Gateway"`
|
Gateway string `json:"gateway"`
|
||||||
Scope string `json:"scope"`
|
IPRange string `json:"ipRange"`
|
||||||
Labels []string `json:"labels"`
|
Labels []string `json:"labels"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Volume struct {
|
type Volume struct {
|
||||||
|
@ -5,8 +5,7 @@ import "time"
|
|||||||
type ImageInfo struct {
|
type ImageInfo struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
Name string `json:"name"`
|
Tags []string `json:"tags"`
|
||||||
Version string `json:"version"`
|
|
||||||
Size string `json:"size"`
|
Size string `json:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,10 +13,6 @@ type ImageLoad struct {
|
|||||||
Path string `josn:"path" validate:"required"`
|
Path string `josn:"path" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageRemove struct {
|
|
||||||
ImageName string `josn:"imageName" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImageBuild struct {
|
type ImageBuild struct {
|
||||||
From string `josn:"from" validate:"required"`
|
From string `josn:"from" validate:"required"`
|
||||||
Dockerfile string `josn:"dockerfile" validate:"required"`
|
Dockerfile string `josn:"dockerfile" validate:"required"`
|
||||||
@ -29,14 +24,20 @@ type ImagePull struct {
|
|||||||
ImageName string `josn:"imageName" validate:"required"`
|
ImageName string `josn:"imageName" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImageTag struct {
|
||||||
|
RepoID uint `josn:"repoID"`
|
||||||
|
SourceID string `json:"sourceID" validate:"required"`
|
||||||
|
TargetName string `josn:"targetName" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type ImagePush struct {
|
type ImagePush struct {
|
||||||
RepoID uint `josn:"repoID" validate:"required"`
|
RepoID uint `josn:"repoID" validate:"required"`
|
||||||
ImageName string `josn:"imageName" validate:"required"`
|
TagName string `json:"tagName" validate:"required"`
|
||||||
TagName string `json:"tagName" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageSave struct {
|
type ImageSave struct {
|
||||||
ImageName string `josn:"imageName" validate:"required"`
|
TagName string `json:"tagName" validate:"required"`
|
||||||
Path string `josn:"path" validate:"required"`
|
Path string `josn:"path" validate:"required"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
}
|
}
|
||||||
|
@ -178,28 +178,20 @@ func (u *ContainerService) PageNetwork(req dto.PageInfo) (int64, interface{}, er
|
|||||||
for key, val := range item.Labels {
|
for key, val := range item.Labels {
|
||||||
tag = append(tag, fmt.Sprintf("%s=%s", key, val))
|
tag = append(tag, fmt.Sprintf("%s=%s", key, val))
|
||||||
}
|
}
|
||||||
var (
|
var ipam network.IPAMConfig
|
||||||
ipv4 network.IPAMConfig
|
if len(item.IPAM.Config) > 0 {
|
||||||
ipv6 network.IPAMConfig
|
ipam = item.IPAM.Config[0]
|
||||||
)
|
|
||||||
if len(item.IPAM.Config) > 1 {
|
|
||||||
ipv4 = item.IPAM.Config[0]
|
|
||||||
ipv6 = item.IPAM.Config[1]
|
|
||||||
} else if len(item.IPAM.Config) > 0 {
|
|
||||||
ipv4 = item.IPAM.Config[0]
|
|
||||||
}
|
}
|
||||||
data = append(data, dto.Network{
|
data = append(data, dto.Network{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
CreatedAt: item.Created,
|
CreatedAt: item.Created,
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
Driver: item.Driver,
|
Driver: item.Driver,
|
||||||
IPAMDriver: item.IPAM.Driver,
|
IPAMDriver: item.IPAM.Driver,
|
||||||
IPV4Subnet: ipv4.Subnet,
|
Subnet: ipam.Subnet,
|
||||||
IPV4Gateway: ipv4.Gateway,
|
Gateway: ipam.Gateway,
|
||||||
IPV6Subnet: ipv6.Subnet,
|
Attachable: item.Attachable,
|
||||||
IPV6Gateway: ipv6.Gateway,
|
Labels: tag,
|
||||||
Attachable: item.Attachable,
|
|
||||||
Labels: tag,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,19 +214,31 @@ func (u *ContainerService) CreateNetwork(req dto.NetworkCreat) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ipv4 := network.IPAMConfig{
|
var (
|
||||||
Subnet: req.IPV4Subnet,
|
ipam network.IPAMConfig
|
||||||
Gateway: req.IPV4Gateway,
|
hasConf bool
|
||||||
|
)
|
||||||
|
if len(req.Subnet) != 0 {
|
||||||
|
ipam.Subnet = req.Subnet
|
||||||
|
hasConf = true
|
||||||
}
|
}
|
||||||
|
if len(req.Gateway) != 0 {
|
||||||
|
ipam.Gateway = req.Gateway
|
||||||
|
hasConf = true
|
||||||
|
}
|
||||||
|
if len(req.IPRange) != 0 {
|
||||||
|
ipam.IPRange = req.IPRange
|
||||||
|
hasConf = true
|
||||||
|
}
|
||||||
|
|
||||||
options := types.NetworkCreate{
|
options := types.NetworkCreate{
|
||||||
Driver: req.Driver,
|
Driver: req.Driver,
|
||||||
Scope: req.Scope,
|
|
||||||
IPAM: &network.IPAM{
|
|
||||||
Config: []network.IPAMConfig{ipv4},
|
|
||||||
},
|
|
||||||
Options: stringsToMap(req.Options),
|
Options: stringsToMap(req.Options),
|
||||||
Labels: stringsToMap(req.Labels),
|
Labels: stringsToMap(req.Labels),
|
||||||
}
|
}
|
||||||
|
if hasConf {
|
||||||
|
options.IPAM = &network.IPAM{Config: []network.IPAMConfig{ipam}}
|
||||||
|
}
|
||||||
if _, err := client.NetworkCreate(context.TODO(), req.Name, options); err != nil {
|
if _, err := client.NetworkCreate(context.TODO(), req.Name, options); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/app/dto"
|
"github.com/1Panel-dev/1Panel/app/dto"
|
||||||
@ -25,7 +24,7 @@ type IImageService interface {
|
|||||||
ImageLoad(req dto.ImageLoad) error
|
ImageLoad(req dto.ImageLoad) error
|
||||||
ImageSave(req dto.ImageSave) error
|
ImageSave(req dto.ImageSave) error
|
||||||
ImagePush(req dto.ImagePush) error
|
ImagePush(req dto.ImagePush) error
|
||||||
ImageRemove(req dto.ImageRemove) error
|
ImageRemove(req dto.BatchDelete) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIImageService() IImageService {
|
func NewIImageService() IImageService {
|
||||||
@ -48,17 +47,12 @@ func (u *ImageService) Page(req dto.PageInfo) (int64, interface{}, error) {
|
|||||||
|
|
||||||
for _, image := range list {
|
for _, image := range list {
|
||||||
size := formatFileSize(image.Size)
|
size := formatFileSize(image.Size)
|
||||||
for _, item := range image.RepoTags {
|
records = append(records, dto.ImageInfo{
|
||||||
name := item[0:strings.LastIndex(item, ":")]
|
ID: image.ID,
|
||||||
tag := strings.ReplaceAll(item[strings.LastIndex(item, ":"):], ":", "")
|
Tags: image.RepoTags,
|
||||||
records = append(records, dto.ImageInfo{
|
CreatedAt: time.Unix(image.Created, 0),
|
||||||
ID: image.ID,
|
Size: size,
|
||||||
Name: name,
|
})
|
||||||
Version: tag,
|
|
||||||
CreatedAt: time.Unix(image.Created, 0),
|
|
||||||
Size: size,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||||
if start > total {
|
if start > total {
|
||||||
@ -164,27 +158,39 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ImageService) ImageSave(req dto.ImageSave) error {
|
func (u *ImageService) ImageSave(req dto.ImageSave) error {
|
||||||
file, err := os.OpenFile(fmt.Sprintf("%s/%s.tar", req.Path, req.Name), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := client.ImageSave(context.TODO(), []string{req.ImageName})
|
out, err := client.ImageSave(context.TODO(), []string{req.TagName})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer out.Close()
|
defer out.Close()
|
||||||
|
file, err := os.OpenFile(fmt.Sprintf("%s/%s.tar", req.Path, req.Name), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
if _, err = io.Copy(file, out); err != nil {
|
if _, err = io.Copy(file, out); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ImageService) ImageTag(req dto.ImageTag) error {
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.ImageTag(context.TODO(), req.SourceID, req.TargetName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ImageService) ImagePush(req dto.ImagePush) error {
|
func (u *ImageService) ImagePush(req dto.ImagePush) error {
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -207,34 +213,36 @@ func (u *ImageService) ImagePush(req dto.ImagePush) error {
|
|||||||
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
||||||
options.RegistryAuth = authStr
|
options.RegistryAuth = authStr
|
||||||
}
|
}
|
||||||
newName := fmt.Sprintf("%s/%s", repo.DownloadUrl, req.TagName)
|
newName := fmt.Sprintf("%s/%s", repo.DownloadUrl, req.Name)
|
||||||
if newName != req.ImageName {
|
if newName != req.TagName {
|
||||||
if err := client.ImageTag(context.TODO(), req.ImageName, newName); err != nil {
|
if err := client.ImageTag(context.TODO(), req.TagName, newName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
out, err := client.ImagePush(context.TODO(), newName, options)
|
out, err := client.ImagePush(context.TODO(), newName, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.LOG.Errorf("image %s push failed, err: %v", req.ImageName, err)
|
global.LOG.Errorf("image %s push failed, err: %v", req.TagName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer out.Close()
|
defer out.Close()
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
_, _ = buf.ReadFrom(out)
|
_, _ = buf.ReadFrom(out)
|
||||||
global.LOG.Debugf("image %s push stdout: %v", req.ImageName, buf.String())
|
global.LOG.Debugf("image %s push stdout: %v", req.TagName, buf.String())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ImageService) ImageRemove(req dto.ImageRemove) error {
|
func (u *ImageService) ImageRemove(req dto.BatchDelete) error {
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := client.ImageRemove(context.TODO(), req.ImageName, types.ImageRemoveOptions{Force: true}); err != nil {
|
for _, ids := range req.Ids {
|
||||||
return err
|
if _, err := client.ImageRemove(context.TODO(), ids, types.ImageRemoveOptions{Force: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -8,13 +8,10 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"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/filters"
|
|
||||||
"github.com/docker/docker/pkg/archive"
|
"github.com/docker/docker/pkg/archive"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -95,24 +92,8 @@ func TestNetwork(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
var data []dto.Volume
|
_, err = client.NetworkCreate(context.TODO(), "test", types.NetworkCreate{})
|
||||||
list, err := client.VolumeList(context.TODO(), filters.NewArgs())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
for _, item := range list.Volumes {
|
|
||||||
tag := make([]string, 0)
|
|
||||||
for _, val := range item.Labels {
|
|
||||||
tag = append(tag, val)
|
|
||||||
}
|
|
||||||
createTime, _ := time.Parse("2006-01-02T15:04:05Z", item.CreatedAt)
|
|
||||||
data = append(data, dto.Volume{
|
|
||||||
CreatedAt: createTime,
|
|
||||||
Name: item.Name,
|
|
||||||
Driver: item.Driver,
|
|
||||||
Mountpoint: item.Mountpoint,
|
|
||||||
Labels: tag,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fmt.Println(data)
|
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
|
|||||||
baRouter.POST("/image/save", baseApi.ImageSave)
|
baRouter.POST("/image/save", baseApi.ImageSave)
|
||||||
baRouter.POST("/image/load", baseApi.ImageLoad)
|
baRouter.POST("/image/load", baseApi.ImageLoad)
|
||||||
baRouter.POST("/image/remove", baseApi.ImageRemove)
|
baRouter.POST("/image/remove", baseApi.ImageRemove)
|
||||||
|
baRouter.POST("/image/tag", baseApi.ImageTag)
|
||||||
|
|
||||||
baRouter.POST("/network/del", baseApi.DeleteNetwork)
|
baRouter.POST("/network/del", baseApi.DeleteNetwork)
|
||||||
baRouter.POST("/network/search", baseApi.SearchNetwork)
|
baRouter.POST("/network/search", baseApi.SearchNetwork)
|
||||||
|
@ -21,7 +21,7 @@ export namespace Container {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
tags: Array<string>;
|
||||||
size: string;
|
size: string;
|
||||||
}
|
}
|
||||||
export interface ImageBuild {
|
export interface ImageBuild {
|
||||||
@ -32,19 +32,20 @@ export namespace Container {
|
|||||||
repoID: number;
|
repoID: number;
|
||||||
imageName: string;
|
imageName: string;
|
||||||
}
|
}
|
||||||
|
export interface ImageTag {
|
||||||
|
repoID: number;
|
||||||
|
sourceID: string;
|
||||||
|
targetName: string;
|
||||||
|
}
|
||||||
export interface ImagePush {
|
export interface ImagePush {
|
||||||
repoID: number;
|
repoID: number;
|
||||||
imageName: string;
|
|
||||||
tagName: string;
|
tagName: string;
|
||||||
}
|
}
|
||||||
export interface ImageRemove {
|
|
||||||
imageName: string;
|
|
||||||
}
|
|
||||||
export interface ImageLoad {
|
export interface ImageLoad {
|
||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
export interface ImageSave {
|
export interface ImageSave {
|
||||||
imageName: string;
|
tagName: string;
|
||||||
path: string;
|
path: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
@ -55,10 +56,8 @@ export namespace Container {
|
|||||||
labels: Array<string>;
|
labels: Array<string>;
|
||||||
driver: string;
|
driver: string;
|
||||||
ipamDriver: string;
|
ipamDriver: string;
|
||||||
ipv4Subnet: string;
|
subnet: string;
|
||||||
ipv4Gateway: string;
|
gateway: string;
|
||||||
ipv6Subnet: string;
|
|
||||||
ipv6Gateway: string;
|
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
attachable: string;
|
attachable: string;
|
||||||
}
|
}
|
||||||
@ -67,8 +66,8 @@ export namespace Container {
|
|||||||
labels: Array<string>;
|
labels: Array<string>;
|
||||||
options: Array<string>;
|
options: Array<string>;
|
||||||
driver: string;
|
driver: string;
|
||||||
ipv4Subnet: string;
|
subnet: string;
|
||||||
ipv4Gateway: string;
|
gateway: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +82,7 @@ export namespace Container {
|
|||||||
name: string;
|
name: string;
|
||||||
driver: string;
|
driver: string;
|
||||||
options: Array<string>;
|
options: Array<string>;
|
||||||
label: Array<string>;
|
labels: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RepoCreate {
|
export interface RepoCreate {
|
||||||
|
@ -23,21 +23,24 @@ export const getImagePage = (params: ReqPage) => {
|
|||||||
return http.post<ResPage<Container.ImageInfo>>(`/containers/image/search`, params);
|
return http.post<ResPage<Container.ImageInfo>>(`/containers/image/search`, params);
|
||||||
};
|
};
|
||||||
export const imageBuild = (params: Container.ImageBuild) => {
|
export const imageBuild = (params: Container.ImageBuild) => {
|
||||||
return http.post<string>(`/containers/image/build`, params);
|
return http.post(`/containers/image/build`, params);
|
||||||
};
|
};
|
||||||
export const imagePull = (params: Container.ImagePull) => {
|
export const imagePull = (params: Container.ImagePull) => {
|
||||||
return http.post<string>(`/containers/image/pull`, params);
|
return http.post(`/containers/image/pull`, params);
|
||||||
};
|
};
|
||||||
export const imagePush = (params: Container.ImagePush) => {
|
export const imagePush = (params: Container.ImagePush) => {
|
||||||
return http.post<string>(`/containers/image/push`, params);
|
return http.post(`/containers/image/push`, params);
|
||||||
};
|
};
|
||||||
export const imageLoad = (params: Container.ImageLoad) => {
|
export const imageLoad = (params: Container.ImageLoad) => {
|
||||||
return http.post<string>(`/containers/image/load`, params);
|
return http.post(`/containers/image/load`, params);
|
||||||
};
|
};
|
||||||
export const imageSave = (params: Container.ImageSave) => {
|
export const imageSave = (params: Container.ImageSave) => {
|
||||||
return http.post<string>(`/containers/image/save`, params);
|
return http.post(`/containers/image/save`, params);
|
||||||
};
|
};
|
||||||
export const imageRemove = (params: Container.ImageRemove) => {
|
export const imageTag = (params: Container.ImageTag) => {
|
||||||
|
return http.post(`/containers/image/tag`, params);
|
||||||
|
};
|
||||||
|
export const imageRemove = (params: Container.BatchDelete) => {
|
||||||
return http.post(`/containers/image/remove`, params);
|
return http.post(`/containers/image/remove`, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -161,8 +161,6 @@ export default {
|
|||||||
reName: 'ReName',
|
reName: 'ReName',
|
||||||
remove: 'Remove',
|
remove: 'Remove',
|
||||||
container: 'Container',
|
container: 'Container',
|
||||||
network: 'Network',
|
|
||||||
storage: 'Storage',
|
|
||||||
schedule: 'Schedule',
|
schedule: 'Schedule',
|
||||||
upTime: 'UpTime',
|
upTime: 'UpTime',
|
||||||
all: 'All',
|
all: 'All',
|
||||||
@ -178,18 +176,33 @@ export default {
|
|||||||
imageName: 'Image name',
|
imageName: 'Image name',
|
||||||
pull: 'Pull',
|
pull: 'Pull',
|
||||||
path: 'Path',
|
path: 'Path',
|
||||||
importImage: 'Import image',
|
importImage: 'Image import',
|
||||||
import: 'Import',
|
import: 'Import',
|
||||||
build: 'Build',
|
build: 'Build',
|
||||||
label: 'Label',
|
label: 'Label',
|
||||||
push: 'Push',
|
push: 'Push',
|
||||||
fileName: 'FileName',
|
fileName: 'FileName',
|
||||||
export: 'Export',
|
export: 'Export',
|
||||||
exportImage: 'ExportImage',
|
exportImage: 'Image export',
|
||||||
version: 'Version',
|
version: 'Version',
|
||||||
size: 'Size',
|
size: 'Size',
|
||||||
from: 'From',
|
from: 'From',
|
||||||
|
|
||||||
|
network: 'Network',
|
||||||
|
createNetwork: 'Create network',
|
||||||
|
networkName: 'Name',
|
||||||
|
driver: 'Driver',
|
||||||
|
option: 'Option',
|
||||||
|
attachable: 'Attachable',
|
||||||
|
subnet: 'Subnet',
|
||||||
|
scope: 'IP Scope',
|
||||||
|
gateway: 'Gateway',
|
||||||
|
|
||||||
|
volume: 'Volume',
|
||||||
|
volumeName: 'Name',
|
||||||
|
mountpoint: 'Mountpoint',
|
||||||
|
createVolume: 'Create volume',
|
||||||
|
|
||||||
repo: 'Repo',
|
repo: 'Repo',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
protocol: 'protocol',
|
protocol: 'protocol',
|
||||||
|
@ -158,7 +158,6 @@ export default {
|
|||||||
reName: '重命名',
|
reName: '重命名',
|
||||||
remove: '移除',
|
remove: '移除',
|
||||||
container: '容器',
|
container: '容器',
|
||||||
storage: '数据卷',
|
|
||||||
schedule: '编排',
|
schedule: '编排',
|
||||||
upTime: '运行时长',
|
upTime: '运行时长',
|
||||||
all: '全部',
|
all: '全部',
|
||||||
@ -168,8 +167,9 @@ export default {
|
|||||||
last10Min: '最近 10 分钟',
|
last10Min: '最近 10 分钟',
|
||||||
|
|
||||||
image: '镜像',
|
image: '镜像',
|
||||||
imagePull: '镜像拉取',
|
imagePull: '拉取镜像',
|
||||||
imagePush: '镜像推送',
|
imagePush: '推送镜像',
|
||||||
|
imageDelete: '删除镜像',
|
||||||
repoName: '仓库名',
|
repoName: '仓库名',
|
||||||
imageName: '镜像名',
|
imageName: '镜像名',
|
||||||
pull: '拉取',
|
pull: '拉取',
|
||||||
@ -199,6 +199,11 @@ export default {
|
|||||||
scope: 'IP 范围',
|
scope: 'IP 范围',
|
||||||
gateway: '网关',
|
gateway: '网关',
|
||||||
|
|
||||||
|
volume: '存储卷',
|
||||||
|
volumeName: '名称',
|
||||||
|
mountpoint: '挂载点',
|
||||||
|
createVolume: '创建存储卷',
|
||||||
|
|
||||||
repo: '仓库',
|
repo: '仓库',
|
||||||
name: '名称',
|
name: '名称',
|
||||||
protocol: '协议',
|
protocol: '协议',
|
||||||
|
86
frontend/src/views/container/image/build/index.vue
Normal file
86
frontend/src/views/container/image/build/index.vue
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="buildVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="50%">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>{{ $t('container.importImage') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form ref="formRef" :model="form" label-width="80px">
|
||||||
|
<el-form-item label="Dockerfile" :rules="Rules.requiredSelect" prop="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-group>
|
||||||
|
</el-form-item>
|
||||||
|
<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-form-item>
|
||||||
|
<el-form-item v-else :rules="Rules.requiredInput">
|
||||||
|
<el-input clearable v-model="form.dockerfile">
|
||||||
|
<template #append>
|
||||||
|
<FileList @choose="loadBuildDir" :dir="true"></FileList>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('container.tag')">
|
||||||
|
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" v-model="form.tag" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="onSubmit(formRef)">{{ $t('container.import') }}</el-button>
|
||||||
|
<el-button @click="buildVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import FileList from '@/components/file-list/index.vue';
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { Rules } from '@/global/form-rules';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { ElForm, ElMessage } from 'element-plus';
|
||||||
|
import { imageBuild } from '@/api/modules/container';
|
||||||
|
|
||||||
|
const buildVisiable = ref(false);
|
||||||
|
const form = reactive({
|
||||||
|
from: 'path',
|
||||||
|
dockerfile: '',
|
||||||
|
tag: '',
|
||||||
|
});
|
||||||
|
const acceptParams = async () => {
|
||||||
|
buildVisiable.value = true;
|
||||||
|
form.from = 'path';
|
||||||
|
form.dockerfile = '';
|
||||||
|
form.tag = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
try {
|
||||||
|
buildVisiable.value = false;
|
||||||
|
await imageBuild(form);
|
||||||
|
emit('search');
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
} catch {
|
||||||
|
emit('search');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadBuildDir = async (path: string) => {
|
||||||
|
form.dockerfile = path;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
acceptParams,
|
||||||
|
});
|
||||||
|
</script>
|
@ -12,14 +12,19 @@
|
|||||||
<el-button @click="onOpenBuild">
|
<el-button @click="onOpenBuild">
|
||||||
{{ $t('container.build') }}
|
{{ $t('container.build') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="danger" plain :disabled="selects.length === 0" @click="onBatchDelete(null)">
|
<el-button type="danger" plain :disabled="selects.length === 0" @click="batchDelete('byid')">
|
||||||
{{ $t('commons.button.delete') }}
|
{{ $t('commons.button.delete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
<el-table-column type="selection" fix></el-table-column>
|
<el-table-column type="selection" fix></el-table-column>
|
||||||
<el-table-column label="ID" show-overflow-tooltip prop="id" min-width="60" />
|
<el-table-column label="ID" show-overflow-tooltip prop="id" min-width="60" />
|
||||||
<el-table-column :label="$t('commons.table.name')" show-overflow-tooltip prop="name" min-width="100" />
|
<el-table-column :label="$t('container.tag')" prop="tags" min-width="160" fix>
|
||||||
<el-table-column :label="$t('container.version')" prop="version" min-width="60" fix />
|
<template #default="{ row }">
|
||||||
|
<el-tag style="margin-left: 5px" v-for="(item, index) of row.tags" :key="index">
|
||||||
|
{{ item }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column :label="$t('container.size')" prop="size" min-width="70" fix />
|
<el-table-column :label="$t('container.size')" prop="size" min-width="70" fix />
|
||||||
<el-table-column :label="$t('commons.table.createdAt')" min-width="80" fix>
|
<el-table-column :label="$t('commons.table.createdAt')" min-width="80" fix>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@ -30,160 +35,32 @@
|
|||||||
</ComplexTable>
|
</ComplexTable>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-dialog v-model="buildVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="50%">
|
<Pull ref="dialogPullRef" @search="search" />
|
||||||
<template #header>
|
<Tag ref="dialogTagRef" @search="search" />
|
||||||
<div class="card-header">
|
<Push ref="dialogPushRef" @search="search" />
|
||||||
<span>{{ $t('container.importImage') }}</span>
|
<Save ref="dialogSaveRef" @search="search" />
|
||||||
</div>
|
<Load ref="dialogLoadRef" @search="search" />
|
||||||
</template>
|
<Build ref="dialogBuildRef" @search="search" />
|
||||||
<el-form ref="buildFormRef" :model="buildForm" label-position="left" label-width="80px">
|
|
||||||
<el-form-item label="Dockerfile" :rules="Rules.requiredSelect" prop="from">
|
|
||||||
<el-radio-group v-model="buildForm.from">
|
|
||||||
<el-radio label="edit">{{ $t('container.edit') }}</el-radio>
|
|
||||||
<el-radio label="path">{{ $t('container.pathSelect') }}</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="buildForm.from !== 'edit'" :rules="Rules.requiredInput">
|
|
||||||
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 10 }" v-model="buildForm.dockerfile" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-else :rules="Rules.requiredInput">
|
|
||||||
<el-input clearable v-model="buildForm.dockerfile">
|
|
||||||
<template #append>
|
|
||||||
<FileList @choose="loadBuildDir" :dir="true"></FileList>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('container.tag')">
|
|
||||||
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" v-model="buildForm.tag" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="submitBuild(buildFormRef)">{{ $t('container.import') }}</el-button>
|
|
||||||
<el-button @click="buildVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog v-model="pullVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
|
<el-dialog v-model="deleteVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>{{ $t('container.imagePull') }}</span>
|
<span>{{ $t('container.imageDelete') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-form ref="pullFormRef" :model="pullForm" label-width="80px">
|
<el-form :model="deleteForm" label-width="80px">
|
||||||
<el-form-item :label="$t('container.from')">
|
<el-form-item label="Tag" prop="tagName">
|
||||||
<el-checkbox v-model="pullForm.fromRepo">{{ $t('container.imageRepo') }}</el-checkbox>
|
<el-checkbox-group v-model="deleteForm.deleteTags">
|
||||||
</el-form-item>
|
<el-checkbox v-for="item in deleteForm.tags" :key="item" :value="item" :label="item" />
|
||||||
<el-form-item
|
</el-checkbox-group>
|
||||||
v-if="pullForm.fromRepo"
|
|
||||||
:label="$t('container.repoName')"
|
|
||||||
:rules="Rules.requiredSelect"
|
|
||||||
prop="repoID"
|
|
||||||
>
|
|
||||||
<el-select style="width: 100%" filterable v-model="pullForm.repoID">
|
|
||||||
<el-option v-for="item in repos" :key="item.id" :value="item.id" :label="item.name" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('container.imageName')" :rules="Rules.requiredInput" prop="imageName">
|
|
||||||
<el-input v-model="pullForm.imageName">
|
|
||||||
<template v-if="pullForm.fromRepo" #prepend>{{ loadDetailInfo(pullForm.repoID) }}/</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
<el-button @click="submitPull(pullFormRef)">
|
<el-button :disabled="deleteForm.deleteTags.length === 0" @click="batchDelete('byname')">
|
||||||
{{ $t('container.pull') }}
|
{{ $t('commons.button.delete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="pullVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
<el-button @click="deleteVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog v-model="pushVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="50%">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span>{{ $t('container.imagePush') }} ({{ pushForm.imageName }})</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<el-form ref="pushFormRef" :model="pushForm" label-width="80px">
|
|
||||||
<el-form-item :label="$t('container.repoName')" :rules="Rules.requiredSelect" prop="repoID">
|
|
||||||
<el-select style="width: 100%" filterable v-model="pushForm.repoID">
|
|
||||||
<el-option v-for="item in repos" :key="item.id" :value="item.id" :label="item.name" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('container.label')" :rules="Rules.requiredInput" prop="tagName">
|
|
||||||
<el-input v-model="pushForm.tagName">
|
|
||||||
<template #prepend>{{ loadDetailInfo(pushForm.repoID) }}/</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="submitPush(pushFormRef)">
|
|
||||||
{{ $t('container.push') }}
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="pushVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog v-model="saveVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="50%">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span>{{ $t('container.exportImage') }} ({{ saveForm.imageName }})</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<el-form ref="saveFormRef" :model="saveForm" label-width="80px">
|
|
||||||
<el-form-item :label="$t('container.path')" :rules="Rules.requiredSelect" prop="path">
|
|
||||||
<el-input clearable v-model="saveForm.path">
|
|
||||||
<template #append>
|
|
||||||
<FileList @choose="loadSaveDir" :dir="true"></FileList>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('container.fileName')" :rules="Rules.requiredInput" prop="name">
|
|
||||||
<el-input v-model="saveForm.name">
|
|
||||||
<template #append>.tar</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="saveForm.path !== '' && saveForm.name !== ''">
|
|
||||||
<el-tag>docker save {{ saveForm.imageName }} > {{ saveForm.path }}/{{ saveForm.name }}.tar</el-tag>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="submitSave(saveFormRef)">
|
|
||||||
{{ $t('container.export') }}
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="saveVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog v-model="loadVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span>{{ $t('container.importImage') }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<el-form ref="loadFormRef" :model="loadForm" label-width="80px">
|
|
||||||
<el-form-item :label="$t('container.path')" :rules="Rules.requiredSelect" prop="path">
|
|
||||||
<el-input clearable v-model="loadForm.path">
|
|
||||||
<template #append>
|
|
||||||
<FileList @choose="loadLoadDir" :dir="false"></FileList>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="loadForm.path !== ''">
|
|
||||||
<el-tag>docker load < {{ loadForm.path }}</el-tag>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="submitLoad(loadFormRef)">{{ $t('container.import') }}</el-button>
|
|
||||||
<el-button @click="loadVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@ -193,22 +70,18 @@
|
|||||||
<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 { reactive, onMounted, ref } from 'vue';
|
import { reactive, onMounted, ref } from 'vue';
|
||||||
import FileList from '@/components/file-list/index.vue';
|
|
||||||
import { dateFromat } from '@/utils/util';
|
import { dateFromat } from '@/utils/util';
|
||||||
import { Container } from '@/api/interface/container';
|
import { Container } from '@/api/interface/container';
|
||||||
import {
|
import Pull from '@/views/container/image/pull/index.vue';
|
||||||
getImagePage,
|
import Tag from '@/views/container/image/tag/index.vue';
|
||||||
getRepoOption,
|
import Push from '@/views/container/image/push/index.vue';
|
||||||
imageBuild,
|
import Save from '@/views/container/image/save/index.vue';
|
||||||
imageLoad,
|
import Load from '@/views/container/image/load/index.vue';
|
||||||
imagePull,
|
import Build from '@/views/container/image/build/index.vue';
|
||||||
imagePush,
|
import { getImagePage, getRepoOption, imageRemove } from '@/api/modules/container';
|
||||||
imageRemove,
|
|
||||||
imageSave,
|
|
||||||
} from '@/api/modules/container';
|
|
||||||
import { Rules } from '@/global/form-rules';
|
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { ElForm, ElMessage, ElMessageBox } from 'element-plus';
|
import { ElForm } from 'element-plus';
|
||||||
|
import { useDeleteData } from '@/hooks/use-delete-data';
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
@ -221,44 +94,17 @@ const paginationConfig = reactive({
|
|||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormInstance = InstanceType<typeof ElForm>;
|
const dialogPullRef = ref();
|
||||||
|
const dialogTagRef = ref();
|
||||||
|
const dialogPushRef = ref();
|
||||||
|
const dialogLoadRef = ref();
|
||||||
|
const dialogSaveRef = ref();
|
||||||
|
const dialogBuildRef = ref();
|
||||||
|
|
||||||
const buildVisiable = ref(false);
|
const deleteVisiable = ref(false);
|
||||||
const buildFormRef = ref<FormInstance>();
|
const deleteForm = reactive({
|
||||||
const buildForm = reactive({
|
deleteTags: [] as Array<string>,
|
||||||
from: 'path',
|
tags: [] as Array<string>,
|
||||||
dockerfile: '',
|
|
||||||
tag: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const pullVisiable = ref(false);
|
|
||||||
const pullFormRef = ref<FormInstance>();
|
|
||||||
const pullForm = reactive({
|
|
||||||
fromRepo: true,
|
|
||||||
repoID: 1,
|
|
||||||
imageName: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const pushVisiable = ref(false);
|
|
||||||
const pushFormRef = ref<FormInstance>();
|
|
||||||
const pushForm = reactive({
|
|
||||||
repoID: 1,
|
|
||||||
imageName: '',
|
|
||||||
tagName: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const saveVisiable = ref(false);
|
|
||||||
const saveFormRef = ref<FormInstance>();
|
|
||||||
const saveForm = reactive({
|
|
||||||
imageName: '',
|
|
||||||
path: '',
|
|
||||||
name: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadVisiable = ref(false);
|
|
||||||
const loadFormRef = ref<FormInstance>();
|
|
||||||
const loadForm = reactive({
|
|
||||||
path: '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const search = async () => {
|
const search = async () => {
|
||||||
@ -278,183 +124,73 @@ const loadRepos = async () => {
|
|||||||
repos.value = res.data;
|
repos.value = res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadBuildDir = async (path: string) => {
|
|
||||||
buildForm.dockerfile = path;
|
|
||||||
};
|
|
||||||
const loadSaveDir = async (path: string) => {
|
|
||||||
saveForm.path = path;
|
|
||||||
};
|
|
||||||
const loadLoadDir = async (path: string) => {
|
|
||||||
loadForm.path = path;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onOpenPull = () => {
|
const onOpenPull = () => {
|
||||||
pullVisiable.value = true;
|
let params = {
|
||||||
pullForm.imageName = '';
|
repos: repos,
|
||||||
pullForm.repoID = 1;
|
};
|
||||||
};
|
dialogPullRef.value!.acceptParams(params);
|
||||||
const submitPull = async (formEl: FormInstance | undefined) => {
|
|
||||||
if (!formEl) return;
|
|
||||||
formEl.validate(async (valid) => {
|
|
||||||
if (!valid) return;
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
if (!pullForm.fromRepo) {
|
|
||||||
pullForm.repoID = 0;
|
|
||||||
}
|
|
||||||
pullVisiable.value = false;
|
|
||||||
await imagePull(pullForm);
|
|
||||||
loading.value = false;
|
|
||||||
search();
|
|
||||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
|
||||||
} catch {
|
|
||||||
loading.value = false;
|
|
||||||
search();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitPush = async (formEl: FormInstance | undefined) => {
|
|
||||||
if (!formEl) return;
|
|
||||||
formEl.validate(async (valid) => {
|
|
||||||
if (!valid) return;
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
pushVisiable.value = false;
|
|
||||||
await imagePush(pushForm);
|
|
||||||
loading.value = false;
|
|
||||||
search();
|
|
||||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
|
||||||
} catch {
|
|
||||||
loading.value = false;
|
|
||||||
search();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onOpenBuild = () => {
|
const onOpenBuild = () => {
|
||||||
buildVisiable.value = true;
|
dialogBuildRef.value!.acceptParams();
|
||||||
buildForm.from = 'path';
|
|
||||||
buildForm.dockerfile = '';
|
|
||||||
};
|
|
||||||
const submitBuild = async (formEl: FormInstance | undefined) => {
|
|
||||||
if (!formEl) return;
|
|
||||||
formEl.validate(async (valid) => {
|
|
||||||
if (!valid) return;
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
loadVisiable.value = false;
|
|
||||||
await imageBuild(buildForm);
|
|
||||||
loading.value = false;
|
|
||||||
search();
|
|
||||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
|
||||||
} catch {
|
|
||||||
loading.value = false;
|
|
||||||
search();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onOpenload = () => {
|
const onOpenload = () => {
|
||||||
loadVisiable.value = true;
|
dialogLoadRef.value!.acceptParams();
|
||||||
loadForm.path = '';
|
|
||||||
};
|
|
||||||
const submitLoad = async (formEl: FormInstance | undefined) => {
|
|
||||||
if (!formEl) return;
|
|
||||||
formEl.validate(async (valid) => {
|
|
||||||
if (!valid) return;
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
loadVisiable.value = false;
|
|
||||||
await imageLoad(loadForm);
|
|
||||||
loading.value = false;
|
|
||||||
search();
|
|
||||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
|
||||||
} catch {
|
|
||||||
loading.value = false;
|
|
||||||
search();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitSave = async (formEl: FormInstance | undefined) => {
|
const batchDelete = async (option: string) => {
|
||||||
if (!formEl) return;
|
let ids: Array<string> = [];
|
||||||
formEl.validate(async (valid) => {
|
if (option === 'byid') {
|
||||||
if (!valid) return;
|
selects.value.forEach((item: Container.NetworkInfo) => {
|
||||||
try {
|
ids.push(item.id);
|
||||||
loading.value = true;
|
});
|
||||||
saveVisiable.value = false;
|
} else {
|
||||||
await imageSave(saveForm);
|
for (const item of deleteForm.deleteTags) {
|
||||||
loading.value = false;
|
ids.push(item);
|
||||||
search();
|
|
||||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
|
||||||
} catch {
|
|
||||||
loading.value = false;
|
|
||||||
search();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onBatchDelete = async (row: Container.ImageInfo | null) => {
|
|
||||||
ElMessageBox.confirm(i18n.global.t('commons.msg.delete'), i18n.global.t('commons.msg.deleteTitle'), {
|
|
||||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
|
||||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
|
||||||
type: 'info',
|
|
||||||
}).then(async () => {
|
|
||||||
if (row) {
|
|
||||||
loading.value = true;
|
|
||||||
await imageRemove({ imageName: row.name + ':' + row.version });
|
|
||||||
loading.value = false;
|
|
||||||
search();
|
|
||||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let ps = [];
|
|
||||||
for (const item of selects.value) {
|
|
||||||
ps.push(imageRemove({ imageName: item.name + ':' + item.version }));
|
|
||||||
}
|
|
||||||
loading.value = true;
|
|
||||||
Promise.all(ps)
|
|
||||||
.then(() => {
|
|
||||||
loading.value = false;
|
|
||||||
search();
|
|
||||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
loading.value = false;
|
|
||||||
search();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadDetailInfo(id: number) {
|
|
||||||
for (const item of repos.value) {
|
|
||||||
if (item.id === id) {
|
|
||||||
return item.downloadUrl;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return '';
|
await useDeleteData(imageRemove, { ids: ids }, 'commons.msg.delete', true);
|
||||||
}
|
deleteVisiable.value = false;
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
|
||||||
const buttons = [
|
const buttons = [
|
||||||
|
{
|
||||||
|
label: 'Tag',
|
||||||
|
click: (row: Container.ImageInfo) => {
|
||||||
|
let params = {
|
||||||
|
repos: repos.value,
|
||||||
|
sourceID: row.id,
|
||||||
|
};
|
||||||
|
dialogTagRef.value!.acceptParams(params);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: i18n.global.t('container.push'),
|
label: i18n.global.t('container.push'),
|
||||||
click: (row: Container.ImageInfo) => {
|
click: (row: Container.ImageInfo) => {
|
||||||
pushForm.imageName = row.name + ':' + row.version;
|
let params = {
|
||||||
pushVisiable.value = true;
|
repos: repos.value,
|
||||||
|
tags: row.tags,
|
||||||
|
};
|
||||||
|
dialogPushRef.value!.acceptParams(params);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.global.t('container.export'),
|
label: i18n.global.t('container.export'),
|
||||||
click: (row: Container.ImageInfo) => {
|
click: (row: Container.ImageInfo) => {
|
||||||
saveForm.imageName = row.name + ':' + row.version;
|
let params = {
|
||||||
saveVisiable.value = true;
|
repos: repos.value,
|
||||||
|
tags: row.tags,
|
||||||
|
};
|
||||||
|
dialogSaveRef.value!.acceptParams(params);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.global.t('commons.button.delete'),
|
label: i18n.global.t('commons.button.delete'),
|
||||||
click: (row: Container.ImageInfo) => {
|
click: (row: Container.ImageInfo) => {
|
||||||
onBatchDelete(row);
|
deleteForm.tags = row.tags;
|
||||||
|
deleteVisiable.value = true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
71
frontend/src/views/container/image/load/index.vue
Normal file
71
frontend/src/views/container/image/load/index.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="loadVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>{{ $t('container.importImage') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form ref="formRef" :model="form" label-width="80px">
|
||||||
|
<el-form-item :label="$t('container.path')" :rules="Rules.requiredSelect" prop="path">
|
||||||
|
<el-input clearable v-model="form.path">
|
||||||
|
<template #append>
|
||||||
|
<FileList @choose="loadLoadDir" :dir="false"></FileList>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="onSubmit(formRef)">{{ $t('container.import') }}</el-button>
|
||||||
|
<el-button @click="loadVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import FileList from '@/components/file-list/index.vue';
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { Rules } from '@/global/form-rules';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { ElForm, ElMessage } from 'element-plus';
|
||||||
|
import { imageLoad } from '@/api/modules/container';
|
||||||
|
|
||||||
|
const loadVisiable = ref(false);
|
||||||
|
const form = reactive({
|
||||||
|
path: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const acceptParams = () => {
|
||||||
|
loadVisiable.value = true;
|
||||||
|
form.path = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
try {
|
||||||
|
loadVisiable.value = false;
|
||||||
|
await imageLoad(form);
|
||||||
|
emit('search');
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
} catch {
|
||||||
|
emit('search');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadLoadDir = async (path: string) => {
|
||||||
|
form.path = path;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
acceptParams,
|
||||||
|
});
|
||||||
|
</script>
|
104
frontend/src/views/container/image/pull/index.vue
Normal file
104
frontend/src/views/container/image/pull/index.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="pullVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>{{ $t('container.imagePull') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form ref="formRef" :model="form" label-width="80px">
|
||||||
|
<el-form-item :label="$t('container.from')">
|
||||||
|
<el-checkbox v-model="form.fromRepo">{{ $t('container.imageRepo') }}</el-checkbox>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item
|
||||||
|
v-if="form.fromRepo"
|
||||||
|
:label="$t('container.repoName')"
|
||||||
|
:rules="Rules.requiredSelect"
|
||||||
|
prop="repoID"
|
||||||
|
>
|
||||||
|
<el-select style="width: 100%" filterable v-model="form.repoID">
|
||||||
|
<el-option v-for="item in dialogData.repos" :key="item.id" :value="item.id" :label="item.name" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('container.imageName')" :rules="Rules.requiredInput" prop="imageName">
|
||||||
|
<el-input v-model="form.imageName">
|
||||||
|
<template v-if="form.fromRepo" #prepend>{{ loadDetailInfo(form.repoID) }}/</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="onSubmit(formRef)">
|
||||||
|
{{ $t('container.pull') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="pullVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { Rules } from '@/global/form-rules';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { ElForm, ElMessage } from 'element-plus';
|
||||||
|
import { imagePull } from '@/api/modules/container';
|
||||||
|
import { Container } from '@/api/interface/container';
|
||||||
|
|
||||||
|
const pullVisiable = ref(false);
|
||||||
|
const form = reactive({
|
||||||
|
fromRepo: true,
|
||||||
|
repoID: 1,
|
||||||
|
imageName: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
interface DialogProps {
|
||||||
|
repos: Array<Container.RepoOptions>;
|
||||||
|
}
|
||||||
|
const dialogData = ref<DialogProps>({
|
||||||
|
repos: [] as Array<Container.RepoOptions>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||||
|
pullVisiable.value = true;
|
||||||
|
form.fromRepo = true;
|
||||||
|
form.repoID = 1;
|
||||||
|
form.imageName = '';
|
||||||
|
dialogData.value.repos = params.repos;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
try {
|
||||||
|
if (!form.fromRepo) {
|
||||||
|
form.repoID = 0;
|
||||||
|
}
|
||||||
|
pullVisiable.value = false;
|
||||||
|
await imagePull(form);
|
||||||
|
emit('search');
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
} catch {
|
||||||
|
emit('search');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadDetailInfo(id: number) {
|
||||||
|
for (const item of dialogData.value.repos) {
|
||||||
|
if (item.id === id) {
|
||||||
|
return item.downloadUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
acceptParams,
|
||||||
|
});
|
||||||
|
</script>
|
102
frontend/src/views/container/image/push/index.vue
Normal file
102
frontend/src/views/container/image/push/index.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="pushVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>{{ $t('container.imagePush') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form ref="formRef" :model="form" label-width="80px">
|
||||||
|
<el-form-item label="Tag" :rules="Rules.requiredSelect" prop="tagName">
|
||||||
|
<el-select filterable v-model="form.tagName">
|
||||||
|
<el-option v-for="item in form.tags" :key="item" :value="item" :label="item" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('container.repoName')" :rules="Rules.requiredSelect" prop="repoID">
|
||||||
|
<el-select style="width: 100%" filterable v-model="form.repoID">
|
||||||
|
<el-option v-for="item in dialogData.repos" :key="item.id" :value="item.id" :label="item.name" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('container.label')" :rules="Rules.requiredInput" prop="name">
|
||||||
|
<el-input v-model="form.name">
|
||||||
|
<template #prepend>{{ loadDetailInfo(form.repoID) }}/</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="onSubmit(formRef)">
|
||||||
|
{{ $t('container.push') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="pushVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { Rules } from '@/global/form-rules';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { ElForm, ElMessage } from 'element-plus';
|
||||||
|
import { imagePush } from '@/api/modules/container';
|
||||||
|
import { Container } from '@/api/interface/container';
|
||||||
|
|
||||||
|
const pushVisiable = ref(false);
|
||||||
|
const form = reactive({
|
||||||
|
tags: [] as Array<string>,
|
||||||
|
tagName: '',
|
||||||
|
repoID: 1,
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
interface DialogProps {
|
||||||
|
repos: Array<Container.RepoOptions>;
|
||||||
|
tags: Array<string>;
|
||||||
|
}
|
||||||
|
const dialogData = ref<DialogProps>({
|
||||||
|
repos: [] as Array<Container.RepoOptions>,
|
||||||
|
tags: [] as Array<string>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||||
|
pushVisiable.value = true;
|
||||||
|
form.tags = params.tags;
|
||||||
|
form.repoID = 1;
|
||||||
|
form.tagName = '';
|
||||||
|
form.name = '';
|
||||||
|
dialogData.value.repos = params.repos;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
try {
|
||||||
|
pushVisiable.value = false;
|
||||||
|
await imagePush(form);
|
||||||
|
emit('search');
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
} catch {
|
||||||
|
emit('search');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadDetailInfo(id: number) {
|
||||||
|
for (const item of dialogData.value.repos) {
|
||||||
|
if (item.id === id) {
|
||||||
|
return item.downloadUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
acceptParams,
|
||||||
|
});
|
||||||
|
</script>
|
106
frontend/src/views/container/image/save/index.vue
Normal file
106
frontend/src/views/container/image/save/index.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="saveVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>{{ $t('container.exportImage') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form ref="formRef" :model="form" label-width="80px">
|
||||||
|
<el-form-item label="Tag" :rules="Rules.requiredSelect" prop="tagName">
|
||||||
|
<el-select filterable v-model="form.tagName">
|
||||||
|
<el-option
|
||||||
|
:disabled="item.indexOf(':<none>') !== -1"
|
||||||
|
v-for="item in form.tags"
|
||||||
|
:key="item"
|
||||||
|
:value="item"
|
||||||
|
:label="item"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('container.path')" :rules="Rules.requiredSelect" prop="path">
|
||||||
|
<el-input clearable v-model="form.path">
|
||||||
|
<template #append>
|
||||||
|
<FileList @choose="loadSaveDir" :dir="true"></FileList>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('container.fileName')" :rules="Rules.requiredInput" prop="name">
|
||||||
|
<el-input v-model="form.name">
|
||||||
|
<template #append>.tar</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="onSubmit(formRef)">
|
||||||
|
{{ $t('container.export') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="saveVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import FileList from '@/components/file-list/index.vue';
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { Rules } from '@/global/form-rules';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { ElForm, ElMessage } from 'element-plus';
|
||||||
|
import { imageSave } from '@/api/modules/container';
|
||||||
|
import { Container } from '@/api/interface/container';
|
||||||
|
|
||||||
|
const saveVisiable = ref(false);
|
||||||
|
const form = reactive({
|
||||||
|
tags: [] as Array<string>,
|
||||||
|
tagName: '',
|
||||||
|
path: '',
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
interface DialogProps {
|
||||||
|
repos: Array<Container.RepoOptions>;
|
||||||
|
tags: Array<string>;
|
||||||
|
}
|
||||||
|
const dialogData = ref<DialogProps>({
|
||||||
|
repos: [] as Array<Container.RepoOptions>,
|
||||||
|
tags: [] as Array<string>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||||
|
saveVisiable.value = true;
|
||||||
|
form.tags = params.tags;
|
||||||
|
form.path = '';
|
||||||
|
form.tagName = '';
|
||||||
|
form.name = '';
|
||||||
|
dialogData.value.repos = params.repos;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
try {
|
||||||
|
saveVisiable.value = false;
|
||||||
|
await imageSave(form);
|
||||||
|
emit('search');
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
} catch {
|
||||||
|
emit('search');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadSaveDir = async (path: string) => {
|
||||||
|
form.path = path;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
acceptParams,
|
||||||
|
});
|
||||||
|
</script>
|
106
frontend/src/views/container/image/tag/index.vue
Normal file
106
frontend/src/views/container/image/tag/index.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="tagVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>Tag {{ $t('container.image') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form ref="formRef" :model="form" label-width="80px">
|
||||||
|
<el-form-item :label="$t('container.from')">
|
||||||
|
<el-checkbox v-model="form.fromRepo">{{ $t('container.imageRepo') }}</el-checkbox>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item
|
||||||
|
v-if="form.fromRepo"
|
||||||
|
:label="$t('container.repoName')"
|
||||||
|
:rules="Rules.requiredSelect"
|
||||||
|
prop="repoID"
|
||||||
|
>
|
||||||
|
<el-select style="width: 100%" filterable v-model="form.repoID">
|
||||||
|
<el-option v-for="item in dialogData.repos" :key="item.id" :value="item.id" :label="item.name" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('container.imageName')" :rules="Rules.requiredInput" prop="targetName">
|
||||||
|
<el-input v-model="form.targetName">
|
||||||
|
<template v-if="form.fromRepo" #prepend>{{ loadDetailInfo(form.repoID) }}/</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="onSubmit(formRef)">{{ $t('commons.button.save') }}</el-button>
|
||||||
|
<el-button @click="tagVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { Rules } from '@/global/form-rules';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { ElForm, ElMessage } from 'element-plus';
|
||||||
|
import { imageTag } from '@/api/modules/container';
|
||||||
|
import { Container } from '@/api/interface/container';
|
||||||
|
|
||||||
|
const tagVisiable = ref(false);
|
||||||
|
const form = reactive({
|
||||||
|
sourceID: '',
|
||||||
|
fromRepo: true,
|
||||||
|
repoID: 1,
|
||||||
|
targetName: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
interface DialogProps {
|
||||||
|
repos: Array<Container.RepoOptions>;
|
||||||
|
sourceID: string;
|
||||||
|
}
|
||||||
|
const dialogData = ref<DialogProps>({
|
||||||
|
repos: [] as Array<Container.RepoOptions>,
|
||||||
|
sourceID: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||||
|
tagVisiable.value = true;
|
||||||
|
form.repoID = 1;
|
||||||
|
form.sourceID = params.sourceID;
|
||||||
|
form.targetName = '';
|
||||||
|
form.fromRepo = true;
|
||||||
|
dialogData.value.repos = params.repos;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
try {
|
||||||
|
if (!form.fromRepo) {
|
||||||
|
form.repoID = 0;
|
||||||
|
}
|
||||||
|
tagVisiable.value = false;
|
||||||
|
await imageTag(form);
|
||||||
|
emit('search');
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
} catch {
|
||||||
|
emit('search');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadDetailInfo(id: number) {
|
||||||
|
for (const item of dialogData.value.repos) {
|
||||||
|
if (item.id === id) {
|
||||||
|
return item.downloadUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
acceptParams,
|
||||||
|
});
|
||||||
|
</script>
|
@ -11,8 +11,8 @@
|
|||||||
<el-radio-button class="topButton" size="large" label="network">
|
<el-radio-button class="topButton" size="large" label="network">
|
||||||
{{ $t('container.network') }}
|
{{ $t('container.network') }}
|
||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
<el-radio-button class="topButton" size="large" label="storage">
|
<el-radio-button class="topButton" size="large" label="volume">
|
||||||
{{ $t('container.storage') }}
|
{{ $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" label="repo">
|
||||||
{{ $t('container.repo') }}
|
{{ $t('container.repo') }}
|
||||||
@ -26,7 +26,7 @@
|
|||||||
<Repo v-if="activeNames === 'repo'" />
|
<Repo v-if="activeNames === 'repo'" />
|
||||||
<Image v-if="activeNames === 'image'" />
|
<Image v-if="activeNames === 'image'" />
|
||||||
<Network v-if="activeNames === 'network'" />
|
<Network v-if="activeNames === 'network'" />
|
||||||
<Monitor v-if="activeNames === 'storage'" />
|
<Volume v-if="activeNames === 'volume'" />
|
||||||
<About v-if="activeNames === 'schedule'" />
|
<About v-if="activeNames === 'schedule'" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -37,7 +37,7 @@ import Container from '@/views/container/container/index.vue';
|
|||||||
import Repo from '@/views/container/repo/index.vue';
|
import Repo from '@/views/container/repo/index.vue';
|
||||||
import Image from '@/views/container/image/index.vue';
|
import Image from '@/views/container/image/index.vue';
|
||||||
import Network from '@/views/container/network/index.vue';
|
import Network from '@/views/container/network/index.vue';
|
||||||
import Monitor from '@/views/setting/tabs/monitor.vue';
|
import Volume from '@/views/container/volume/index.vue';
|
||||||
import About from '@/views/setting/tabs/about.vue';
|
import About from '@/views/setting/tabs/about.vue';
|
||||||
|
|
||||||
const activeNames = ref('container');
|
const activeNames = ref('container');
|
||||||
|
@ -20,11 +20,11 @@
|
|||||||
<el-form-item :label="$t('container.option')" prop="optionStr">
|
<el-form-item :label="$t('container.option')" prop="optionStr">
|
||||||
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" v-model="form.optionStr" />
|
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" v-model="form.optionStr" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('container.subnet')" prop="ipv4Subnet">
|
<el-form-item :label="$t('container.subnet')" prop="subnet">
|
||||||
<el-input clearable v-model="form.ipv4Subnet" />
|
<el-input clearable v-model="form.subnet" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('container.gateway')" prop="ipv4Gateway">
|
<el-form-item :label="$t('container.gateway')" prop="gateway">
|
||||||
<el-input clearable v-model="form.ipv4Gateway" />
|
<el-input clearable v-model="form.gateway" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('container.scope')" prop="scope">
|
<el-form-item :label="$t('container.scope')" prop="scope">
|
||||||
<el-input clearable v-model="form.scope" />
|
<el-input clearable v-model="form.scope" />
|
||||||
@ -59,8 +59,8 @@ const form = reactive({
|
|||||||
optionStr: '',
|
optionStr: '',
|
||||||
options: [] as Array<string>,
|
options: [] as Array<string>,
|
||||||
driver: '',
|
driver: '',
|
||||||
ipv4Subnet: '',
|
subnet: '',
|
||||||
ipv4Gateway: '',
|
gateway: '',
|
||||||
scope: '',
|
scope: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -73,9 +73,6 @@ const emit = defineEmits<{ (e: 'search'): void }>();
|
|||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
name: [Rules.requiredInput, Rules.name],
|
name: [Rules.requiredInput, Rules.name],
|
||||||
driver: [Rules.requiredSelect],
|
driver: [Rules.requiredSelect],
|
||||||
ipv4Subnet: [Rules.requiredInput],
|
|
||||||
ipv4Gateway: [Rules.requiredInput],
|
|
||||||
scope: [Rules.requiredInput],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormInstance = InstanceType<typeof ElForm>;
|
type FormInstance = InstanceType<typeof ElForm>;
|
||||||
|
@ -25,30 +25,8 @@
|
|||||||
<el-icon color="red" v-if="!row.attachable"><CloseBold /></el-icon>
|
<el-icon color="red" v-if="!row.attachable"><CloseBold /></el-icon>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column :label="$t('container.subnet')" min-width="80" fix>
|
<el-table-column :label="$t('container.subnet')" min-width="80" prop="subnet" fix />
|
||||||
<template #default="{ row }">
|
<el-table-column :label="$t('container.gateway')" min-width="80" prop="gateway" fix />
|
||||||
<div v-if="row.ipv4Subnet.length !== 0">
|
|
||||||
ipv4
|
|
||||||
<el-tag>{{ row.ipv4Subnet }}</el-tag>
|
|
||||||
</div>
|
|
||||||
<div v-if="row.ipv6Subnet.length !== 0">
|
|
||||||
ipv6
|
|
||||||
<el-tag>{{ row.ipv6Subnet }}</el-tag>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column :label="$t('container.gateway')" min-width="80" fix>
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div v-if="row.ipv4Gateway.length !== 0">
|
|
||||||
ipv4
|
|
||||||
<el-tag>{{ row.ipv4Gateway }}</el-tag>
|
|
||||||
</div>
|
|
||||||
<div v-if="row.ipv6Gateway.length !== 0">
|
|
||||||
ipv6
|
|
||||||
<el-tag>{{ row.ipv6Gateway }}</el-tag>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column :label="$t('container.tag')" min-width="140" fix>
|
<el-table-column :label="$t('container.tag')" min-width="140" fix>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div v-for="(item, index) of row.labels" :key="index">
|
<div v-for="(item, index) of row.labels" :key="index">
|
||||||
|
92
frontend/src/views/container/volume/create/index.vue
Normal file
92
frontend/src/views/container/volume/create/index.vue
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="createVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>{{ $t('container.createVolume') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||||
|
<el-form-item :label="$t('container.volumeName')" prop="name">
|
||||||
|
<el-input clearable v-model="form.name" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('container.driver')" prop="driver">
|
||||||
|
<el-select v-model="form.driver">
|
||||||
|
<el-option label="local" value="local" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('container.option')" prop="optionStr">
|
||||||
|
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" v-model="form.optionStr" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('container.tag')" prop="labelStr">
|
||||||
|
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" v-model="form.labelStr" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="createVisiable = 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 { Rules } from '@/global/form-rules';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { ElForm, ElMessage } from 'element-plus';
|
||||||
|
import { createVolume } from '@/api/modules/container';
|
||||||
|
|
||||||
|
const createVisiable = ref(false);
|
||||||
|
const form = reactive({
|
||||||
|
name: '',
|
||||||
|
driver: 'local',
|
||||||
|
labelStr: '',
|
||||||
|
labels: [] as Array<string>,
|
||||||
|
optionStr: '',
|
||||||
|
options: [] as Array<string>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const acceptParams = (): void => {
|
||||||
|
createVisiable.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||||
|
|
||||||
|
const rules = reactive({
|
||||||
|
name: [Rules.requiredInput, Rules.name],
|
||||||
|
driver: [Rules.requiredSelect],
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormInstance = InstanceType<typeof ElForm>;
|
||||||
|
const formRef = ref<FormInstance>();
|
||||||
|
|
||||||
|
function restForm() {
|
||||||
|
if (formRef.value) {
|
||||||
|
formRef.value.resetFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.validate(async (valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
if (form.labelStr !== '') {
|
||||||
|
form.labels = form.labelStr.split('\n');
|
||||||
|
}
|
||||||
|
if (form.optionStr !== '') {
|
||||||
|
form.options = form.optionStr.split('\n');
|
||||||
|
}
|
||||||
|
await createVolume(form);
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
restForm();
|
||||||
|
emit('search');
|
||||||
|
createVisiable.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
acceptParams,
|
||||||
|
});
|
||||||
|
</script>
|
107
frontend/src/views/container/volume/index.vue
Normal file
107
frontend/src/views/container/volume/index.vue
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-card style="margin-top: 20px">
|
||||||
|
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
||||||
|
<template #toolbar>
|
||||||
|
<el-button style="margin-left: 10px" @click="onCreate()">
|
||||||
|
{{ $t('commons.button.create') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" plain :disabled="selects.length === 0" @click="batchDelete(null)">
|
||||||
|
{{ $t('commons.button.delete') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<el-table-column type="selection" fix />
|
||||||
|
<el-table-column
|
||||||
|
:label="$t('commons.table.name')"
|
||||||
|
show-overflow-tooltip
|
||||||
|
min-width="80"
|
||||||
|
prop="name"
|
||||||
|
fix
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
:label="$t('container.mountpoint')"
|
||||||
|
show-overflow-tooltip
|
||||||
|
min-width="120"
|
||||||
|
prop="mountpoint"
|
||||||
|
/>
|
||||||
|
<el-table-column :label="$t('container.driver')" show-overflow-tooltip min-width="80" prop="driver" />
|
||||||
|
<el-table-column
|
||||||
|
prop="createdAt"
|
||||||
|
min-width="90"
|
||||||
|
:label="$t('commons.table.date')"
|
||||||
|
:formatter="dateFromat"
|
||||||
|
/>
|
||||||
|
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
||||||
|
</ComplexTable>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<CreateDialog @search="search" ref="dialogCreateRef" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
|
import CreateDialog from '@/views/container/volume/create/index.vue';
|
||||||
|
import { reactive, onMounted, ref } from 'vue';
|
||||||
|
import { dateFromat } from '@/utils/util';
|
||||||
|
import { deleteVolume, getVolumePage } from '@/api/modules/container';
|
||||||
|
import { Container } from '@/api/interface/container';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { useDeleteData } from '@/hooks/use-delete-data';
|
||||||
|
|
||||||
|
const data = ref();
|
||||||
|
const selects = ref<any>([]);
|
||||||
|
const paginationConfig = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogCreateRef = ref<DialogExpose>();
|
||||||
|
|
||||||
|
interface DialogExpose {
|
||||||
|
acceptParams: () => void;
|
||||||
|
}
|
||||||
|
const onCreate = async () => {
|
||||||
|
dialogCreateRef.value!.acceptParams();
|
||||||
|
};
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
const params = {
|
||||||
|
page: paginationConfig.page,
|
||||||
|
pageSize: paginationConfig.pageSize,
|
||||||
|
};
|
||||||
|
await getVolumePage(params).then((res) => {
|
||||||
|
if (res.data) {
|
||||||
|
data.value = res.data.items;
|
||||||
|
}
|
||||||
|
paginationConfig.total = res.data.total;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const batchDelete = async (row: Container.VolumeInfo | null) => {
|
||||||
|
let ids: Array<string> = [];
|
||||||
|
if (row === null) {
|
||||||
|
selects.value.forEach((item: Container.VolumeInfo) => {
|
||||||
|
ids.push(item.name);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ids.push(row.name);
|
||||||
|
}
|
||||||
|
await useDeleteData(deleteVolume, { ids: ids }, 'commons.msg.delete', true);
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttons = [
|
||||||
|
{
|
||||||
|
label: i18n.global.t('commons.button.delete'),
|
||||||
|
click: (row: Container.VolumeInfo) => {
|
||||||
|
batchDelete(row);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
</script>
|
Loading…
x
Reference in New Issue
Block a user