mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-02 03:24:23 +08:00
feat: Support batch upgrades for container images (#9915)
This commit is contained in:
parent
c49c16cbb5
commit
b156ef9476
16 changed files with 206 additions and 53 deletions
|
@ -57,7 +57,7 @@ func (b *BaseApi) LoadContainerUsers(c *gin.Context) {
|
|||
// @Summary List containers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} string
|
||||
// @Success 200 {array} dto.ContainerOptions
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/list [post]
|
||||
|
@ -65,6 +65,23 @@ func (b *BaseApi) ListContainer(c *gin.Context) {
|
|||
helper.SuccessWithData(c, containerService.List())
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary List containers by image
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} dto.ContainerOptions
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/list/byimage [post]
|
||||
func (b *BaseApi) ListContainerByImage(c *gin.Context) {
|
||||
var req dto.OperationWithName
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, containerService.ListByImage(req.Name))
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary Load containers status
|
||||
// @Accept json
|
||||
|
|
|
@ -107,10 +107,10 @@ type ContainerCreateByCommand struct {
|
|||
}
|
||||
|
||||
type ContainerUpgrade struct {
|
||||
TaskID string `json:"taskID"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Image string `json:"image" validate:"required"`
|
||||
ForcePull bool `json:"forcePull"`
|
||||
TaskID string `json:"taskID"`
|
||||
Names []string `json:"names" validate:"required"`
|
||||
Image string `json:"image" validate:"required"`
|
||||
ForcePull bool `json:"forcePull"`
|
||||
}
|
||||
|
||||
type ContainerListStats struct {
|
||||
|
|
|
@ -54,6 +54,7 @@ type ContainerService struct{}
|
|||
type IContainerService interface {
|
||||
Page(req dto.PageContainer) (int64, interface{}, error)
|
||||
List() []dto.ContainerOptions
|
||||
ListByImage(imageName string) []dto.ContainerOptions
|
||||
LoadStatus() (dto.ContainerStatus, error)
|
||||
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
|
||||
ListNetwork() ([]dto.Options, error)
|
||||
|
@ -246,6 +247,33 @@ func (u *ContainerService) List() []dto.ContainerOptions {
|
|||
return options
|
||||
}
|
||||
|
||||
func (u *ContainerService) ListByImage(imageName string) []dto.ContainerOptions {
|
||||
var options []dto.ContainerOptions
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load docker client for contianer list failed, err: %v", err)
|
||||
return nil
|
||||
}
|
||||
defer client.Close()
|
||||
containers, err := client.ContainerList(context.Background(), container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load container list failed, err: %v", err)
|
||||
return nil
|
||||
}
|
||||
for _, container := range containers {
|
||||
if container.Image != imageName {
|
||||
continue
|
||||
}
|
||||
for _, name := range container.Names {
|
||||
if len(name) != 0 {
|
||||
options = append(options, dto.ContainerOptions{Name: strings.TrimPrefix(name, "/"), State: container.State})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
|
||||
var data dto.ContainerStatus
|
||||
client, err := docker.NewDockerClient()
|
||||
|
@ -683,6 +711,12 @@ func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if len(oldContainer.Config.Entrypoint) != 0 {
|
||||
if oldContainer.Config.Entrypoint[0] == "/docker-entrypoint.sh" {
|
||||
oldContainer.Config.Entrypoint = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpdate, task.TaskScopeContainer, req.TaskID, 1)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("new task for create container failed, err: %v", err)
|
||||
|
@ -745,17 +779,14 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
|
|||
}
|
||||
defer client.Close()
|
||||
ctx := context.Background()
|
||||
oldContainer, err := client.ContainerInspect(ctx, req.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpgrade, task.TaskScopeContainer, req.TaskID, 1)
|
||||
taskItem, err := task.NewTaskWithOps(req.Image, task.TaskUpgrade, task.TaskScopeImage, req.TaskID, 1)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("new task for create container failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error {
|
||||
taskItem.LogStart(i18n.GetWithName("ContainerImagePull", req.Image))
|
||||
if !checkImageExist(client, req.Image) || req.ForcePull {
|
||||
if err := pullImages(taskItem, client, req.Image); err != nil {
|
||||
if !req.ForcePull {
|
||||
|
@ -766,38 +797,58 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
|
|||
}
|
||||
return nil
|
||||
}, nil)
|
||||
|
||||
taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", req.Name), func(t *task.Task) error {
|
||||
config := oldContainer.Config
|
||||
config.Image = req.Image
|
||||
hostConf := oldContainer.HostConfig
|
||||
var networkConf network.NetworkingConfig
|
||||
if oldContainer.NetworkSettings != nil {
|
||||
for networkKey := range oldContainer.NetworkSettings.Networks {
|
||||
networkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
|
||||
break
|
||||
for _, item := range req.Names {
|
||||
var oldContainer container.InspectResponse
|
||||
taskItem.AddSubTask(i18n.GetWithName("ContainerLoadInfo", item), func(t *task.Task) error {
|
||||
taskItem.Logf("----------------- %s -----------------", item)
|
||||
oldContainer, err = client.ContainerInspect(ctx, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err := client.ContainerRemove(ctx, req.Name, container.RemoveOptions{Force: true})
|
||||
taskItem.LogWithStatus(i18n.GetWithName("ContainerRemoveOld", req.Name), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inspected, err := client.ImageInspect(ctx, req.Image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect image failed, err: %v", err)
|
||||
}
|
||||
if isDynamicImage(inspected) {
|
||||
oldContainer.Config.Entrypoint = nil
|
||||
oldContainer.Config.Cmd = nil
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
|
||||
con, err := client.ContainerCreate(ctx, config, hostConf, &networkConf, &v1.Platform{}, req.Name)
|
||||
if err != nil {
|
||||
taskItem.Log(i18n.GetMsgByKey("ContainerRecreate"))
|
||||
reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings)
|
||||
return fmt.Errorf("upgrade container failed, err: %v", err)
|
||||
}
|
||||
err = client.ContainerStart(ctx, con.ID, container.StartOptions{})
|
||||
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err)
|
||||
if err != nil {
|
||||
return fmt.Errorf("upgrade successful but start failed, err: %v", err)
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", item), func(t *task.Task) error {
|
||||
oldContainer.Config.Cmd = nil
|
||||
config := oldContainer.Config
|
||||
config.Image = req.Image
|
||||
hostConf := oldContainer.HostConfig
|
||||
var networkConf network.NetworkingConfig
|
||||
if oldContainer.NetworkSettings != nil {
|
||||
for networkKey := range oldContainer.NetworkSettings.Networks {
|
||||
networkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
|
||||
break
|
||||
}
|
||||
}
|
||||
err := client.ContainerRemove(ctx, item, container.RemoveOptions{Force: true})
|
||||
taskItem.LogWithStatus(i18n.GetWithName("ContainerRemoveOld", item), err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
con, err := client.ContainerCreate(ctx, config, hostConf, &networkConf, &v1.Platform{}, item)
|
||||
if err != nil {
|
||||
taskItem.Log(i18n.GetMsgByKey("ContainerRecreate"))
|
||||
reCreateAfterUpdate(item, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings)
|
||||
return fmt.Errorf("upgrade container failed, err: %v", err)
|
||||
}
|
||||
err = client.ContainerStart(ctx, con.ID, container.StartOptions{})
|
||||
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err)
|
||||
if err != nil {
|
||||
return fmt.Errorf("upgrade successful but start failed, err: %v", err)
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
|
||||
}
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
global.LOG.Error(err.Error())
|
||||
}
|
||||
|
@ -1692,3 +1743,23 @@ func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper {
|
|||
}
|
||||
return exposedPorts
|
||||
}
|
||||
|
||||
func isDynamicImage(inspected image.InspectResponse) bool {
|
||||
if len(inspected.Config.Entrypoint) > 0 {
|
||||
entrypointStr := strings.Join(inspected.Config.Entrypoint, " ")
|
||||
if strings.Contains(entrypointStr, "entrypoint") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
dirs := []string{"/docker-entrypoint.d", "/docker-entrypoint-initdb.d"}
|
||||
for _, dir := range dirs {
|
||||
for _, layer := range inspected.RootFS.Layers {
|
||||
if strings.Contains(layer, dir) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
|
|||
baRouter.POST("/info", baseApi.ContainerInfo)
|
||||
baRouter.POST("/search", baseApi.SearchContainer)
|
||||
baRouter.POST("/list", baseApi.ListContainer)
|
||||
baRouter.POST("/list/byimage", baseApi.ListContainerByImage)
|
||||
baRouter.GET("/status", baseApi.LoadContainerStatus)
|
||||
baRouter.GET("/list/stats", baseApi.ContainerListStats)
|
||||
baRouter.GET("/search/log", baseApi.ContainerStreamLogs)
|
||||
|
|
|
@ -45,6 +45,10 @@ export namespace Container {
|
|||
|
||||
imageSize: number;
|
||||
}
|
||||
export interface ContainerOption {
|
||||
name: string;
|
||||
state: string;
|
||||
}
|
||||
export interface ResourceLimit {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
|
@ -86,7 +90,7 @@ export namespace Container {
|
|||
}
|
||||
export interface ContainerUpgrade {
|
||||
taskID: string;
|
||||
name: string;
|
||||
names: Array<string>;
|
||||
image: string;
|
||||
forcePull: boolean;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,10 @@ export const searchContainer = (params: Container.ContainerSearch) => {
|
|||
return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const listContainer = () => {
|
||||
return http.post<Array<Container.ContainerInfo>>(`/containers/list`, {});
|
||||
return http.post<Array<Container.ContainerOption>>(`/containers/list`, {});
|
||||
};
|
||||
export const listContainerByImage = (image: string) => {
|
||||
return http.post<Array<Container.ContainerOption>>(`/containers/list/byimage`, { name: image });
|
||||
};
|
||||
export const loadContainerUsers = (name: string) => {
|
||||
return http.post<Array<string>>(`/containers/users`, { name: name });
|
||||
|
|
|
@ -805,6 +805,8 @@ const message = {
|
|||
upgradeWarning2:
|
||||
'The upgrade operation requires rebuilding the container, any unpersisted data will be lost. Do you wish to continue?',
|
||||
oldImage: 'Current image',
|
||||
sameImageContainer: 'Same-image containers',
|
||||
sameImageHelper: 'Containers using the same image can be batch upgraded after selection',
|
||||
targetImage: 'Target image',
|
||||
imageLoadErr: 'No image name detected for the container',
|
||||
appHelper: 'The container comes from the app store, and upgrading may make the service unavailable.',
|
||||
|
|
|
@ -783,6 +783,8 @@ const message = {
|
|||
upgradeWarning2: 'アップグレード操作では、コンテナを再構築する必要があります。続けたいですか?',
|
||||
oldImage: '現在の画像',
|
||||
targetImage: 'ターゲット画像',
|
||||
sameImageContainer: '同一イメージコンテナ',
|
||||
sameImageHelper: '同一イメージを使用するコンテナは選択後一括アップグレード可能',
|
||||
imageLoadErr: 'コンテナの画像名は検出されません',
|
||||
appHelper:
|
||||
'このコンテナはアプリストアから取得されたものであり、アップグレードによってサービスが利用不可になる可能性があります。',
|
||||
|
|
|
@ -777,6 +777,8 @@ const message = {
|
|||
upgradeWarning2:
|
||||
'업그레이드 작업은 컨테이너를 재빌드해야 하며, 비지속적인 데이터가 손실됩니다. 계속하시겠습니까?',
|
||||
oldImage: '현재 이미지',
|
||||
sameImageContainer: '동일 이미지 컨테이너',
|
||||
sameImageHelper: '동일한 이미지를 사용하는 컨테이너는 선택 후 일괄 업그레이드 가능',
|
||||
targetImage: '대상 이미지',
|
||||
imageLoadErr: '컨테이너에 대한 이미지 이름이 감지되지 않았습니다.',
|
||||
appHelper: '이 컨테이너는 앱 스토어에서 왔으며 업그레이드 시 서비스가 중단될 수 있습니다.',
|
||||
|
|
|
@ -798,6 +798,8 @@ const message = {
|
|||
upgradeWarning2:
|
||||
'Operasi peningkatan memerlukan pembinaan semula kontena, sebarang data yang tidak disimpan akan hilang. Adakah anda mahu meneruskan?',
|
||||
oldImage: 'Imej semasa',
|
||||
sameImageContainer: 'Kontena imej sama',
|
||||
sameImageHelper: 'Kontena yang menggunakan imej sama boleh dinaik taraf secara berkumpulan setelah dipilih',
|
||||
targetImage: 'Imej sasaran',
|
||||
imageLoadErr: 'Tiada nama imej dikesan untuk kontena',
|
||||
appHelper:
|
||||
|
|
|
@ -794,6 +794,8 @@ const message = {
|
|||
upgradeWarning2:
|
||||
'A operação de upgrade requer a reconstrução do contêiner, e qualquer dado não persistente será perdido. Deseja continuar?',
|
||||
oldImage: 'Imagem atual',
|
||||
sameImageContainer: 'Contêineres com mesma imagem',
|
||||
sameImageHelper: 'Contêineres usando a mesma imagem podem ser atualizados em lote após seleção',
|
||||
targetImage: 'Imagem alvo',
|
||||
imageLoadErr: 'Nenhum nome de imagem detectado para o contêiner',
|
||||
appHelper: 'O contêiner vem da loja de aplicativos, e o upgrade pode tornar o serviço indisponível.',
|
||||
|
|
|
@ -793,6 +793,8 @@ const message = {
|
|||
upgradeWarning2:
|
||||
'Операция обновления требует пересборки контейнера, все несохраненные данные будут потеряны. Хотите продолжить?',
|
||||
oldImage: 'Текущий образ',
|
||||
sameImageContainer: 'Контейнеры с одинаковым образом',
|
||||
sameImageHelper: 'Контейнеры, использующие один образ, можно массово обновить после выбора',
|
||||
targetImage: 'Целевой образ',
|
||||
imageLoadErr: 'Не обнаружено имя образа для контейнера',
|
||||
appHelper: 'Контейнер происходит из магазина приложений, и обновление может сделать сервис недоступным.',
|
||||
|
|
|
@ -815,6 +815,8 @@ const message = {
|
|||
upgradeWarning2:
|
||||
'Yükseltme işlemi konteynerin yeniden oluşturulmasını gerektirir, kalıcı olmayan tüm veriler kaybedilecektir. Devam etmek istiyor musunuz?',
|
||||
oldImage: 'Mevcut imaj',
|
||||
sameImageContainer: 'Aynı imajlı konteynerler',
|
||||
sameImageHelper: 'Aynı imajı kullanan konteynerlar seçilerek toplu şekilde güncellenebilir',
|
||||
targetImage: 'Hedef imaj',
|
||||
imageLoadErr: 'Konteyner için imaj adı algılanmadı',
|
||||
appHelper: 'Konteyner uygulama mağazasından geliyor ve yükseltme hizmeti kullanılamaz hale getirebilir.',
|
||||
|
|
|
@ -770,6 +770,8 @@ const message = {
|
|||
upgradeHelper: '倉庫名稱/鏡像名稱:鏡像版本',
|
||||
upgradeWarning2: '升級操作需要重建容器,任何未持久化的數據將會丟失,是否繼續?',
|
||||
oldImage: '當前鏡像',
|
||||
sameImageContainer: '同鏡像容器',
|
||||
sameImageHelper: '同鏡像容器可勾選後批量升級',
|
||||
targetImage: '目標鏡像',
|
||||
imageLoadErr: '未檢測到容器的鏡像名稱',
|
||||
appHelper: '該容器來源於應用商店,升級可能導致該服務不可用',
|
||||
|
|
|
@ -769,6 +769,8 @@ const message = {
|
|||
upgradeHelper: '仓库名称/镜像名称:镜像版本',
|
||||
upgradeWarning2: '升级操作需要重建容器,任何未持久化的数据将会丢失,是否继续?',
|
||||
oldImage: '当前镜像',
|
||||
sameImageContainer: '同镜像容器',
|
||||
sameImageHelper: '同镜像容器可勾选后批量升级',
|
||||
targetImage: '目标镜像',
|
||||
imageLoadErr: '未检测到容器的镜像名称',
|
||||
appHelper: '该容器来源于应用商店,升级可能导致该服务不可用',
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
<template>
|
||||
<DrawerPro
|
||||
v-model="drawerVisible"
|
||||
:header="$t('commons.button.upgrade')"
|
||||
@close="handleClose"
|
||||
:resource="form.name"
|
||||
size="large"
|
||||
>
|
||||
<DrawerPro v-model="drawerVisible" :header="$t('commons.button.upgrade')" @close="handleClose" size="large">
|
||||
<el-alert
|
||||
:title="$t('container.appHelper')"
|
||||
v-if="form.fromApp"
|
||||
|
@ -20,6 +14,24 @@
|
|||
</el-tooltip>
|
||||
<el-tag v-else>{{ form.oldImageName }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('container.sameImageContainer')" v-if="containerOptions.length > 1">
|
||||
<div class="w-full">
|
||||
<el-checkbox v-model="checkAll" :indeterminate="isIndeterminate" @change="handleCheckAllChange">
|
||||
{{ $t('commons.table.all') }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<el-checkbox-group v-model="form.names" @change="handleCheckedChange">
|
||||
<el-checkbox
|
||||
v-for="item in containerOptions"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="item.name"
|
||||
>
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<span class="input-help">{{ $t('container.sameImageHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item prop="newImageName" :rules="Rules.imageName">
|
||||
<template #label>
|
||||
{{ $t('container.targetImage') }}
|
||||
|
@ -52,20 +64,20 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { upgradeContainer } from '@/api/modules/container';
|
||||
import { listContainerByImage, upgradeContainer } from '@/api/modules/container';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { newUUID } from '@/utils/util';
|
||||
import { ElForm } from 'element-plus';
|
||||
import { CheckboxValueType, ElForm } from 'element-plus';
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
const loading = ref(false);
|
||||
const taskLogRef = ref();
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
names: [],
|
||||
oldImageName: '',
|
||||
newImageName: '',
|
||||
hasName: true,
|
||||
|
@ -79,13 +91,18 @@ const formRef = ref<FormInstance>();
|
|||
const drawerVisible = ref<boolean>(false);
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
|
||||
const containerOptions = ref([]);
|
||||
const isIndeterminate = ref();
|
||||
const checkAll = ref();
|
||||
|
||||
interface DialogProps {
|
||||
container: string;
|
||||
image: string;
|
||||
fromApp: boolean;
|
||||
}
|
||||
const acceptParams = (props: DialogProps): void => {
|
||||
form.name = props.container;
|
||||
form.names = [props.container];
|
||||
isIndeterminate.value = true;
|
||||
form.oldImageName = props.image;
|
||||
form.fromApp = props.fromApp;
|
||||
form.hasName = props.image.indexOf('sha256:') === -1;
|
||||
|
@ -94,10 +111,32 @@ const acceptParams = (props: DialogProps): void => {
|
|||
} else {
|
||||
form.newImageName = '';
|
||||
}
|
||||
loadContainers();
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const loadContainers = async () => {
|
||||
const res = await listContainerByImage(form.oldImageName);
|
||||
containerOptions.value = res.data || [];
|
||||
};
|
||||
|
||||
const handleCheckAllChange = (val: CheckboxValueType) => {
|
||||
form.names = [];
|
||||
if (!val) {
|
||||
isIndeterminate.value = false;
|
||||
return;
|
||||
}
|
||||
for (const item of containerOptions.value) {
|
||||
form.names.push(item.name);
|
||||
}
|
||||
};
|
||||
const handleCheckedChange = (value: CheckboxValueType[]) => {
|
||||
const checkedCount = value.length;
|
||||
checkAll.value = checkedCount === containerOptions.value.length;
|
||||
isIndeterminate.value = checkedCount > 0 && checkedCount < containerOptions.value.length;
|
||||
};
|
||||
|
||||
const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
|
@ -109,7 +148,7 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
|||
let taskID = newUUID();
|
||||
let param = {
|
||||
taskID: taskID,
|
||||
name: form.name,
|
||||
names: form.names,
|
||||
image: form.newImageName,
|
||||
forcePull: form.forcePull,
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue