mirror of
https://github.com/tnb-labs/panel.git
synced 2026-09-01 14:55:12 +08:00
feat: Docker 操作支持后台任务
This commit is contained in:
+2
-2
@@ -385,7 +385,7 @@ func initAce() (*app.Ace, func(), error) {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
containerUsecase := biz.NewContainerUsecase(containerRepo, settingRepo)
|
||||
containerUsecase := biz.NewContainerUsecase(locale, containerRepo, settingRepo, taskRepo)
|
||||
containerService, err := service.NewContainerService(containerUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
@@ -407,7 +407,7 @@ func initAce() (*app.Ace, func(), error) {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
containerImageUsecase := biz.NewContainerImageUsecase(containerImageRepo, settingRepo)
|
||||
containerImageUsecase := biz.NewContainerImageUsecase(locale, containerImageRepo, settingRepo, taskRepo)
|
||||
containerImageService, err := service.NewContainerImageService(containerImageUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
|
||||
@@ -12,6 +12,7 @@ require (
|
||||
github.com/coreos/go-systemd/v22 v22.7.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/dchest/captcha v1.1.0
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/expr-lang/expr v1.17.8
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/go-chi/chi/v5 v5.3.1
|
||||
@@ -80,7 +81,6 @@ require (
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/boyter/go-string v1.0.5 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
@@ -27,10 +29,17 @@ type ContainerRepo interface {
|
||||
type ContainerUsecase struct {
|
||||
repo ContainerRepo
|
||||
setting SettingRepo
|
||||
task TaskRepo
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewContainerUsecase(repo ContainerRepo, setting SettingRepo) *ContainerUsecase {
|
||||
return &ContainerUsecase{repo: repo, setting: setting}
|
||||
func NewContainerUsecase(t *gotext.Locale, containerRepo ContainerRepo, settingRepo SettingRepo, taskRepo TaskRepo) *ContainerUsecase {
|
||||
return &ContainerUsecase{
|
||||
repo: containerRepo,
|
||||
setting: settingRepo,
|
||||
task: taskRepo,
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) ListAll() ([]types.Container, error) {
|
||||
@@ -62,6 +71,29 @@ func (uc *ContainerUsecase) Create(req *request.ContainerCreate) (string, error)
|
||||
return uc.repo.Create(sock, req)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) CreateBackground(req *request.ContainerCreate) error {
|
||||
shell, err := containerRunShell(containerSock(uc.setting), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
task := new(Task)
|
||||
key := ""
|
||||
if req.Name != "" {
|
||||
key = "container:create:" + req.Name
|
||||
}
|
||||
task.Key = key
|
||||
target := req.Name
|
||||
if target == "" {
|
||||
target = req.Image
|
||||
}
|
||||
task.Name = uc.t.Get("Create container %s", target)
|
||||
task.Status = TaskStatusWaiting
|
||||
task.Shell = shell
|
||||
|
||||
return uc.task.Push(task)
|
||||
}
|
||||
|
||||
// Update 删除旧容器后按新配置重建同名容器
|
||||
func (uc *ContainerUsecase) Update(id string, req *request.ContainerCreate) (string, error) {
|
||||
sock := containerSock(uc.setting)
|
||||
@@ -71,6 +103,33 @@ func (uc *ContainerUsecase) Update(id string, req *request.ContainerCreate) (str
|
||||
return uc.repo.Create(sock, req)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) UpdateBackground(id string, req *request.ContainerCreate) error {
|
||||
sock := containerSock(uc.setting)
|
||||
runShell, err := containerRunShell(sock, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shell := strings.Join([]string{
|
||||
"set -e",
|
||||
dockerCommand(sock, "image", "inspect", req.Image) + " >/dev/null 2>&1 || " + dockerCommand(sock, "pull", req.Image),
|
||||
dockerCommand(sock, "rm", "--force", id),
|
||||
runShell,
|
||||
}, "\n")
|
||||
target := req.Name
|
||||
if target == "" {
|
||||
target = req.Image
|
||||
}
|
||||
|
||||
task := new(Task)
|
||||
task.Key = "container:update:" + id
|
||||
task.Name = uc.t.Get("Update container %s", target)
|
||||
task.Status = TaskStatusWaiting
|
||||
task.Shell = shell
|
||||
|
||||
return uc.task.Push(task)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) Remove(id string) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Remove(sock, id)
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/libtnb/utils/str"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
)
|
||||
|
||||
func containerImagePullShell(sock string, req *request.ContainerImagePull) (string, string, error) {
|
||||
if !req.Auth {
|
||||
return dockerCommand(sock, "pull", req.Name), "", nil
|
||||
}
|
||||
|
||||
named, err := reference.ParseNormalizedNamed(req.Name)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("invalid image reference: %w", err)
|
||||
}
|
||||
|
||||
configDir := filepath.Join(os.TempDir(), "ace-docker-task-"+str.Random(16))
|
||||
cleanup := "rm -rf " + shellQuote(configDir)
|
||||
shell := strings.Join([]string{
|
||||
"set -e",
|
||||
"mkdir -p " + shellQuote(configDir),
|
||||
"trap " + shellQuote(cleanup) + " EXIT",
|
||||
"printf %s " + shellQuote(req.Password) + " | " + dockerCommand(sock, "--config", configDir, "login", "--username", req.Username, "--password-stdin", reference.Domain(named)),
|
||||
dockerCommand(sock, "--config", configDir, "pull", req.Name),
|
||||
}, "\n")
|
||||
|
||||
return shell, cleanup, nil
|
||||
}
|
||||
|
||||
func containerRunShell(sock string, req *request.ContainerCreate) (string, error) {
|
||||
args := []string{"run", "--detach"}
|
||||
|
||||
if req.Name != "" {
|
||||
args = append(args, "--name", req.Name)
|
||||
}
|
||||
if req.Network != "" {
|
||||
args = append(args, "--network", req.Network)
|
||||
}
|
||||
if req.PublishAllPorts {
|
||||
args = append(args, "--publish-all")
|
||||
} else {
|
||||
for _, port := range req.Ports {
|
||||
if port.ContainerStart < 1 || port.ContainerEnd > 65535 || port.ContainerStart > port.ContainerEnd ||
|
||||
port.HostStart < 1 || port.HostEnd > 65535 || port.HostStart > port.HostEnd {
|
||||
return "", errors.New("port range is invalid")
|
||||
}
|
||||
if port.ContainerEnd-port.ContainerStart != port.HostEnd-port.HostStart {
|
||||
return "", errors.New("container port and host port count do not match")
|
||||
}
|
||||
|
||||
for offset := uint(0); offset <= port.HostEnd-port.HostStart; offset++ {
|
||||
host := ""
|
||||
if port.Host.IsValid() {
|
||||
host = port.Host.String() + ":"
|
||||
if port.Host.Is6() {
|
||||
host = "[" + port.Host.String() + "]:"
|
||||
}
|
||||
}
|
||||
mapping := fmt.Sprintf("%s%d:%d/%s", host, port.HostStart+offset, port.ContainerStart+offset, port.Protocol)
|
||||
args = append(args, "--publish", mapping)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, volume := range req.Volumes {
|
||||
args = append(args, "--volume", strings.Join([]string{volume.Host, volume.Container, volume.Mode}, ":"))
|
||||
}
|
||||
for _, env := range req.Env {
|
||||
args = append(args, "--env", env.Key+"="+env.Value)
|
||||
}
|
||||
for _, label := range req.Labels {
|
||||
args = append(args, "--label", label.Key+"="+label.Value)
|
||||
}
|
||||
|
||||
if len(req.Entrypoint) > 0 {
|
||||
args = append(args, "--entrypoint", req.Entrypoint[0])
|
||||
}
|
||||
if req.RestartPolicy != "" {
|
||||
restartPolicy := req.RestartPolicy
|
||||
if restartPolicy == "on-failure" {
|
||||
restartPolicy += ":5"
|
||||
}
|
||||
args = append(args, "--restart", restartPolicy)
|
||||
}
|
||||
if req.AutoRemove {
|
||||
args = append(args, "--rm")
|
||||
}
|
||||
if req.Privileged {
|
||||
args = append(args, "--privileged")
|
||||
}
|
||||
if req.OpenStdin {
|
||||
args = append(args, "--interactive")
|
||||
}
|
||||
if req.Tty {
|
||||
args = append(args, "--tty")
|
||||
}
|
||||
if req.CPUShares > 0 {
|
||||
args = append(args, "--cpu-shares", strconv.FormatInt(req.CPUShares, 10))
|
||||
}
|
||||
if req.CPUs > 0 {
|
||||
args = append(args, "--cpus", strconv.FormatFloat(req.CPUs, 'f', -1, 64))
|
||||
}
|
||||
if req.Memory > 0 {
|
||||
args = append(args, "--memory", strconv.FormatInt(req.Memory, 10)+"m")
|
||||
}
|
||||
|
||||
args = append(args, req.Image)
|
||||
if len(req.Entrypoint) > 1 {
|
||||
args = append(args, req.Entrypoint[1:]...)
|
||||
}
|
||||
args = append(args, req.Command...)
|
||||
|
||||
return dockerCommand(sock, args...), nil
|
||||
}
|
||||
|
||||
func dockerCommand(sock string, args ...string) string {
|
||||
args = append([]string{"docker", "--host", sock}, args...)
|
||||
for i := range args {
|
||||
args[i] = shellQuote(args[i])
|
||||
}
|
||||
return strings.Join(args, " ")
|
||||
}
|
||||
|
||||
func shellQuote(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
@@ -16,10 +18,17 @@ type ContainerImageRepo interface {
|
||||
type ContainerImageUsecase struct {
|
||||
repo ContainerImageRepo
|
||||
setting SettingRepo
|
||||
task TaskRepo
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewContainerImageUsecase(repo ContainerImageRepo, setting SettingRepo) *ContainerImageUsecase {
|
||||
return &ContainerImageUsecase{repo: repo, setting: setting}
|
||||
func NewContainerImageUsecase(t *gotext.Locale, containerImageRepo ContainerImageRepo, settingRepo SettingRepo, taskRepo TaskRepo) *ContainerImageUsecase {
|
||||
return &ContainerImageUsecase{
|
||||
repo: containerImageRepo,
|
||||
setting: settingRepo,
|
||||
task: taskRepo,
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ContainerImageUsecase) List() ([]types.ContainerImage, error) {
|
||||
@@ -37,6 +46,22 @@ func (uc *ContainerImageUsecase) Pull(req *request.ContainerImagePull) error {
|
||||
return uc.repo.Pull(sock, req)
|
||||
}
|
||||
|
||||
func (uc *ContainerImageUsecase) PullBackground(req *request.ContainerImagePull) error {
|
||||
shell, cancelShell, err := containerImagePullShell(containerSock(uc.setting), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
task := new(Task)
|
||||
task.Key = "container:image:pull:" + req.Name
|
||||
task.Name = uc.t.Get("Pull image %s", req.Name)
|
||||
task.Status = TaskStatusWaiting
|
||||
task.Shell = shell
|
||||
task.CancelShell = cancelShell
|
||||
|
||||
return uc.task.Push(task)
|
||||
}
|
||||
|
||||
func (uc *ContainerImageUsecase) Remove(id string) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Remove(sock, id)
|
||||
|
||||
@@ -190,7 +190,7 @@ func (r *containerRepo) Create(sock string, req *request.ContainerCreate) (strin
|
||||
}
|
||||
// 设置资源限制
|
||||
hostConfig.CPUShares = req.CPUShares
|
||||
hostConfig.NanoCPUs = req.CPUs * 1e9
|
||||
hostConfig.NanoCPUs = int64(req.CPUs * 1e9)
|
||||
hostConfig.Memory = req.Memory * 1024 * 1024
|
||||
hostConfig.MemorySwap = 0
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ type ContainerRename struct {
|
||||
type ContainerCreate struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Image string `form:"image" json:"image" validate:"required"`
|
||||
Background bool `form:"background" json:"background"`
|
||||
Ports []types.ContainerPort `form:"ports" json:"ports"`
|
||||
Network string `form:"network" json:"network"`
|
||||
Volumes []types.ContainerContainerVolume `form:"volumes" json:"volumes"`
|
||||
@@ -28,6 +29,6 @@ type ContainerCreate struct {
|
||||
PublishAllPorts bool `form:"publish_all_ports" json:"publish_all_ports"`
|
||||
Tty bool `form:"tty" json:"tty"`
|
||||
CPUShares int64 `form:"cpu_shares" json:"cpu_shares"`
|
||||
CPUs int64 `form:"cpus" json:"cpus"`
|
||||
CPUs float64 `form:"cpus" json:"cpus"`
|
||||
Memory int64 `form:"memory" json:"memory"`
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ type ContainerImageID struct {
|
||||
}
|
||||
|
||||
type ContainerImagePull struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Auth bool `form:"auth" json:"auth"`
|
||||
Username string `form:"username" json:"username" validate:"required_if:Auth,true"`
|
||||
Password string `form:"password" json:"password" validate:"required_if:Auth,true"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Background bool `form:"background" json:"background"`
|
||||
Auth bool `form:"auth" json:"auth"`
|
||||
Username string `form:"username" json:"username" validate:"required_if:Auth,true"`
|
||||
Password string `form:"password" json:"password" validate:"required_if:Auth,true"`
|
||||
}
|
||||
|
||||
@@ -64,7 +64,17 @@ func (s *ContainerService) Update(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := s.containerRepo.Update(chi.URLParam(r, "id"), req)
|
||||
idParam := chi.URLParam(r, "id")
|
||||
if req.Background {
|
||||
if err = s.containerRepo.UpdateBackground(idParam, req); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
Success(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := s.containerRepo.Update(idParam, req)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -80,6 +90,15 @@ func (s *ContainerService) Create(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Background {
|
||||
if err = s.containerRepo.CreateBackground(req); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
Success(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := s.containerRepo.Create(req)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
|
||||
@@ -57,6 +57,15 @@ func (s *ContainerImageService) Pull(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Background {
|
||||
if err = s.containerImageRepo.PullBackground(req); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
Success(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.containerImageRepo.Pull(req); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ func ExecWithLog(ctx context.Context, shell string, logFile string) error {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return fmt.Errorf("run %s failed, err: %w", shell, err)
|
||||
return fmt.Errorf("run shell failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -107,16 +107,23 @@ const getNetworks = () => {
|
||||
}
|
||||
|
||||
// 创建/更新容器
|
||||
const createContainer = () => {
|
||||
const createContainer = (background = false) => {
|
||||
doSubmit.value = true
|
||||
const config = { ...createModel, background }
|
||||
const req = isEdit.value
|
||||
? container.containerUpdate(props.editId!, createModel)
|
||||
: container.containerCreate(createModel)
|
||||
? container.containerUpdate(props.editId!, config)
|
||||
: container.containerCreate(config)
|
||||
useRequest(req)
|
||||
.onSuccess(() => {
|
||||
window.$message.success(
|
||||
isEdit.value ? $gettext('Updated successfully') : $gettext('Created successfully'),
|
||||
)
|
||||
if (background) {
|
||||
window.$message.success(
|
||||
$gettext('Task submitted, please check progress in background tasks'),
|
||||
)
|
||||
} else {
|
||||
window.$message.success(
|
||||
isEdit.value ? $gettext('Updated successfully') : $gettext('Created successfully'),
|
||||
)
|
||||
}
|
||||
show.value = false
|
||||
})
|
||||
.onComplete(() => {
|
||||
@@ -129,6 +136,14 @@ const onPullSuccess = () => {
|
||||
createContainer()
|
||||
}
|
||||
|
||||
const onPullBackground = () => {
|
||||
createContainer(true)
|
||||
}
|
||||
|
||||
const onPullCancel = () => {
|
||||
doSubmit.value = false
|
||||
}
|
||||
|
||||
// 提交处理
|
||||
const handleSubmit = () => {
|
||||
if (!createModel.image) {
|
||||
@@ -646,5 +661,7 @@ watch(show, (val) => {
|
||||
v-model:show="showPullModal"
|
||||
:image="createModel.image"
|
||||
@success="onPullSuccess"
|
||||
@background="onPullBackground"
|
||||
@cancel="onPullCancel"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -13,15 +13,26 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: []
|
||||
background: []
|
||||
cancel: []
|
||||
}>()
|
||||
|
||||
const background = ref(false)
|
||||
const isPulling = ref(false)
|
||||
const pullProgress = ref<Map<string, any>>(new Map())
|
||||
const pullStatus = ref('')
|
||||
const pullError = ref('')
|
||||
let pullWs: WebSocket | null = null
|
||||
|
||||
const closePullSocket = () => {
|
||||
if (!pullWs) return
|
||||
pullWs.onmessage = null
|
||||
pullWs.onclose = null
|
||||
pullWs.onerror = null
|
||||
pullWs.close()
|
||||
pullWs = null
|
||||
}
|
||||
|
||||
// 计算总体拉取进度
|
||||
const totalProgress = computed(() => {
|
||||
const layers = Array.from(pullProgress.value.values())
|
||||
@@ -38,6 +49,8 @@ const totalProgress = computed(() => {
|
||||
|
||||
// 拉取镜像
|
||||
const pullImage = () => {
|
||||
closePullSocket()
|
||||
|
||||
isPulling.value = true
|
||||
pullProgress.value = new Map()
|
||||
pullStatus.value = $gettext('Connecting...')
|
||||
@@ -95,12 +108,19 @@ const pullImage = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (background.value) {
|
||||
show.value = false
|
||||
emit('background')
|
||||
return
|
||||
}
|
||||
|
||||
pullImage()
|
||||
}
|
||||
|
||||
// 取消拉取
|
||||
const cancelPull = () => {
|
||||
if (pullWs) {
|
||||
pullWs.close()
|
||||
pullWs = null
|
||||
}
|
||||
closePullSocket()
|
||||
resetState()
|
||||
show.value = false
|
||||
emit('cancel')
|
||||
@@ -117,27 +137,21 @@ const resetState = () => {
|
||||
watch(show, (val) => {
|
||||
if (val) {
|
||||
resetState()
|
||||
pullImage()
|
||||
background.value = false
|
||||
} else {
|
||||
if (pullWs) {
|
||||
pullWs.close()
|
||||
pullWs = null
|
||||
}
|
||||
closePullSocket()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pullWs) {
|
||||
pullWs.close()
|
||||
pullWs = null
|
||||
}
|
||||
closePullSocket()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
:title="$gettext('Pulling Image')"
|
||||
:title="$gettext('Pull Image')"
|
||||
preset="card"
|
||||
style="width: 60vw"
|
||||
size="medium"
|
||||
@@ -201,5 +215,21 @@ onUnmounted(() => {
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-result>
|
||||
|
||||
<n-form v-else>
|
||||
<n-form-item :label="$gettext('Image Name')">
|
||||
<n-input :value="props.image" readonly />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Execution Mode')">
|
||||
<n-radio-group v-model:value="background">
|
||||
<n-radio-button :value="false">{{ $gettext('Foreground') }}</n-radio-button>
|
||||
<n-radio-button :value="true">{{ $gettext('Background') }}</n-radio-button>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
<n-flex justify="end">
|
||||
<n-button @click="cancelPull">{{ $gettext('Cancel') }}</n-button>
|
||||
<n-button type="primary" @click="handleSubmit">{{ $gettext('Submit') }}</n-button>
|
||||
</n-flex>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
@@ -12,11 +12,13 @@ const { confirmDelete } = useConfirm()
|
||||
|
||||
const pullModel = ref({
|
||||
name: '',
|
||||
background: false,
|
||||
auth: false,
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
const pullModal = ref(false)
|
||||
const pullSubmitting = ref(false)
|
||||
const pruneLoading = ref(false)
|
||||
const selectedRowKeys = ref<any>([])
|
||||
|
||||
@@ -27,6 +29,15 @@ const pullStatus = ref('')
|
||||
const pullError = ref('')
|
||||
let pullWs: WebSocket | null = null
|
||||
|
||||
const closePullSocket = () => {
|
||||
if (!pullWs) return
|
||||
pullWs.onmessage = null
|
||||
pullWs.onclose = null
|
||||
pullWs.onerror = null
|
||||
pullWs.close()
|
||||
pullWs = null
|
||||
}
|
||||
|
||||
// 计算总体拉取进度
|
||||
const totalProgress = computed(() => {
|
||||
const layers = Array.from(pullProgress.value.values())
|
||||
@@ -146,11 +157,9 @@ const handleBulkDelete = async () => {
|
||||
|
||||
// 取消拉取
|
||||
const cancelPull = () => {
|
||||
if (pullWs) {
|
||||
pullWs.close()
|
||||
pullWs = null
|
||||
}
|
||||
closePullSocket()
|
||||
resetState()
|
||||
pullModal.value = false
|
||||
}
|
||||
|
||||
// 重置拉取状态
|
||||
@@ -168,6 +177,23 @@ const handlePull = () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (pullModel.value.background) {
|
||||
pullSubmitting.value = true
|
||||
useRequest(container.imagePull({ ...pullModel.value, background: true }))
|
||||
.onSuccess(() => {
|
||||
pullModal.value = false
|
||||
window.$message.success(
|
||||
$gettext('Task submitted, please check progress in background tasks'),
|
||||
)
|
||||
})
|
||||
.onComplete(() => {
|
||||
pullSubmitting.value = false
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
closePullSocket()
|
||||
|
||||
isPulling.value = true
|
||||
pullProgress.value = new Map()
|
||||
pullStatus.value = $gettext('Connecting...')
|
||||
@@ -233,11 +259,9 @@ const handlePull = () => {
|
||||
watch(pullModal, (val) => {
|
||||
if (val) {
|
||||
resetState()
|
||||
pullModel.value.background = false
|
||||
} else {
|
||||
if (pullWs) {
|
||||
pullWs.close()
|
||||
pullWs = null
|
||||
}
|
||||
closePullSocket()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -246,7 +270,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelPull()
|
||||
closePullSocket()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -304,8 +328,8 @@ onUnmounted(() => {
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
:mask-closable="!isPulling"
|
||||
:closable="!isPulling"
|
||||
:mask-closable="!isPulling && !pullSubmitting"
|
||||
:closable="!isPulling && !pullSubmitting"
|
||||
>
|
||||
<!-- 拉取进度 -->
|
||||
<template v-if="isPulling || pullProgress.size > 0">
|
||||
@@ -379,6 +403,12 @@ onUnmounted(() => {
|
||||
<n-form-item path="auth" :label="$gettext('Authentication')">
|
||||
<n-switch v-model:value="pullModel.auth" />
|
||||
</n-form-item>
|
||||
<n-form-item path="background" :label="$gettext('Execution Mode')">
|
||||
<n-radio-group v-model:value="pullModel.background">
|
||||
<n-radio-button :value="false">{{ $gettext('Foreground') }}</n-radio-button>
|
||||
<n-radio-button :value="true">{{ $gettext('Background') }}</n-radio-button>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="pullModel.auth" path="username" :label="$gettext('Username')">
|
||||
<n-input
|
||||
v-model:value="pullModel.username"
|
||||
@@ -397,7 +427,13 @@ onUnmounted(() => {
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-button type="info" block :loading="loading" :disabled="loading" @click="handlePull">
|
||||
<n-button
|
||||
type="info"
|
||||
block
|
||||
:loading="pullSubmitting"
|
||||
:disabled="pullSubmitting"
|
||||
@click="handlePull"
|
||||
>
|
||||
{{ $gettext('Submit') }}
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user