1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-19 08:19:15 +08:00

feat: 支持直接从命令创建容器,表单样式优化 (#6741)

This commit is contained in:
ssongliu 2024-10-16 22:19:28 +08:00 committed by GitHub
parent 14bbb8c65d
commit 9c5da23a38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 451 additions and 205 deletions

View File

@ -257,6 +257,27 @@ func (b *BaseApi) ContainerCreate(c *gin.Context) {
helper.SuccessWithData(c, nil) helper.SuccessWithData(c, nil)
} }
// @Tags Container
// @Summary Create container by command
// @Description 命令创建容器
// @Accept json
// @Param request body dto.ContainerCreateByCommand true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /containers/command [post]
func (b *BaseApi) ContainerCreateByCommand(c *gin.Context) {
var req dto.ContainerCreateByCommand
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := containerService.ContainerCreateByCommand(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Container // @Tags Container
// @Summary Upgrade container // @Summary Upgrade container
// @Description 更新容器镜像 // @Description 更新容器镜像

View File

@ -90,6 +90,11 @@ type ContainerOperate struct {
RestartPolicy string `json:"restartPolicy"` RestartPolicy string `json:"restartPolicy"`
} }
type ContainerCreateByCommand struct {
TaskID string `json:"taskID"`
Command string `json:"command"`
}
type ContainerUpgrade struct { type ContainerUpgrade struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Image string `json:"image" validate:"required"` Image string `json:"image" validate:"required"`

View File

@ -11,6 +11,7 @@ import (
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"sort" "sort"
"strconv" "strconv"
@ -63,6 +64,7 @@ type IContainerService interface {
CreateCompose(req dto.ComposeCreate) error CreateCompose(req dto.ComposeCreate) error
ComposeOperation(req dto.ComposeOperation) error ComposeOperation(req dto.ComposeOperation) error
ContainerCreate(req dto.ContainerOperate) error ContainerCreate(req dto.ContainerOperate) error
ContainerCreateByCommand(req dto.ContainerCreateByCommand) error
ContainerUpdate(req dto.ContainerOperate) error ContainerUpdate(req dto.ContainerOperate) error
ContainerUpgrade(req dto.ContainerUpgrade) error ContainerUpgrade(req dto.ContainerUpgrade) error
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error) ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
@ -313,6 +315,25 @@ func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error
return datas, nil return datas, nil
} }
func (u *ContainerService) ContainerCreateByCommand(req dto.ContainerCreateByCommand) error {
if cmd.CheckIllegal(req.Command) {
return buserr.New(constant.ErrCmdIllegal)
}
taskItem, err := task.NewTaskWithOps("-", task.TaskCreate, task.TaskScopeContainer, 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("ContainerCreate", "-"), func(t *task.Task) error {
logPath := path.Join(constant.LogDir, task.TaskScopeContainer, req.TaskID+".log")
return cmd.ExecShell(logPath, 5*time.Minute, "bash", "-c", req.Command)
}, nil)
_ = taskItem.Execute()
}()
return nil
}
func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) { func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {

View File

@ -15,6 +15,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.GET("/stats/:id", baseApi.ContainerStats) baRouter.GET("/stats/:id", baseApi.ContainerStats)
baRouter.POST("", baseApi.ContainerCreate) baRouter.POST("", baseApi.ContainerCreate)
baRouter.POST("command", baseApi.ContainerCreateByCommand)
baRouter.POST("/update", baseApi.ContainerUpdate) baRouter.POST("/update", baseApi.ContainerUpdate)
baRouter.POST("/upgrade", baseApi.ContainerUpgrade) baRouter.POST("/upgrade", baseApi.ContainerUpgrade)
baRouter.POST("/info", baseApi.ContainerInfo) baRouter.POST("/info", baseApi.ContainerInfo)

View File

@ -18,6 +18,9 @@ export const loadResourceLimit = () => {
export const createContainer = (params: Container.ContainerHelper) => { export const createContainer = (params: Container.ContainerHelper) => {
return http.post(`/containers`, params, TimeoutEnum.T_10M); return http.post(`/containers`, params, TimeoutEnum.T_10M);
}; };
export const createContainerByCommand = (command: string, taskID: string) => {
return http.post(`/containers/command`, { command: command, taskID: taskID });
};
export const updateContainer = (params: Container.ContainerHelper) => { export const updateContainer = (params: Container.ContainerHelper) => {
return http.post(`/containers/update`, params, TimeoutEnum.T_10M); return http.post(`/containers/update`, params, TimeoutEnum.T_10M);
}; };

View File

@ -37,6 +37,10 @@ const props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
height: {
type: Number,
default: 0,
},
heightDiff: { heightDiff: {
type: Number, type: Number,
default: 200, default: 200,
@ -45,6 +49,10 @@ const props = defineProps({
type: Number, type: Number,
default: 400, default: 400,
}, },
lineWrapping: {
type: Boolean,
default: false,
},
}); });
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
@ -62,7 +70,7 @@ const initCodeMirror = () => {
const defaultTheme = EditorView.theme({ const defaultTheme = EditorView.theme({
'&.cm-editor': { '&.cm-editor': {
minHeight: props.minHeight + 'px', minHeight: props.minHeight + 'px',
height: 'calc(100vh - ' + props.heightDiff + 'px)', height: props.height ? props.height + 'px' : 'calc(100vh - ' + props.heightDiff + 'px)',
}, },
}); });
@ -78,6 +86,9 @@ const initCodeMirror = () => {
placeholder(props.placeholder), placeholder(props.placeholder),
EditorView.editable.of(!props.disabled), EditorView.editable.of(!props.disabled),
]; ];
if (props.lineWrapping) {
extensions.push(EditorView.lineWrapping);
}
switch (props.mode) { switch (props.mode) {
case 'dockerfile': case 'dockerfile':
extensions.push(StreamLanguage.define(dockerFile)); extensions.push(StreamLanguage.define(dockerFile));

View File

@ -580,6 +580,10 @@ const message = {
}, },
container: { container: {
create: 'Create', create: 'Create',
createByCommand: 'Create by command',
commandInput: 'Command input',
commandRule: 'Please enter the correct docker run container creation command!',
commandHelper: 'This command will be executed on the server to create the container. Do you want to continue?',
edit: 'Edit container', edit: 'Edit container',
updateContainerHelper: updateContainerHelper:
'Container editing requires rebuilding the container. Any data that has not been persisted will be lost. Do you want to continue?', 'Container editing requires rebuilding the container. Any data that has not been persisted will be lost. Do you want to continue?',
@ -669,6 +673,7 @@ const message = {
imageLoadErr: 'No image name detected for the container', imageLoadErr: 'No image name detected for the container',
appHelper: 'This container is sourced from the app store; upgrading might render the service unavailable', appHelper: 'This container is sourced from the app store; upgrading might render the service unavailable',
resource: 'Resource',
input: 'Input', input: 'Input',
forcePull: 'forced image pull ', forcePull: 'forced image pull ',
forcePullHelper: 'Ignore existing images on the server and pull again.', forcePullHelper: 'Ignore existing images on the server and pull again.',

View File

@ -562,6 +562,10 @@ const message = {
}, },
container: { container: {
create: '創建容器', create: '創建容器',
createByCommand: '命令創建',
commandInput: '命令輸入',
commandRule: '請輸入正確的 docker run 容器創建命令',
commandHelper: '將在伺服器上執行該條命令以創建容器是否繼續',
edit: '編輯容器', edit: '編輯容器',
updateContainerHelper: '容器編輯需要重建容器任何未持久化的數據將會丟失是否繼續', updateContainerHelper: '容器編輯需要重建容器任何未持久化的數據將會丟失是否繼續',
containerList: '容器列表', containerList: '容器列表',
@ -643,6 +647,7 @@ const message = {
imageLoadErr: '未檢測到容器的鏡像名稱', imageLoadErr: '未檢測到容器的鏡像名稱',
appHelper: '該容器來源於應用商店升級可能導致該服務不可用', appHelper: '該容器來源於應用商店升級可能導致該服務不可用',
resource: '資源',
input: '手動輸入', input: '手動輸入',
forcePull: '強製拉取鏡像', forcePull: '強製拉取鏡像',
forcePullHelper: '忽略服務器已存在的鏡像重新拉取一次', forcePullHelper: '忽略服務器已存在的鏡像重新拉取一次',

View File

@ -562,6 +562,10 @@ const message = {
}, },
container: { container: {
create: '创建容器', create: '创建容器',
createByCommand: '命令创建',
commandInput: '命令输入',
commandRule: '请输入正确的 docker run 容器创建命令',
commandHelper: '将在服务器上执行该条命令以创建容器是否继续',
edit: '编辑容器', edit: '编辑容器',
updateContainerHelper: '容器编辑需要重建容器任何未持久化的数据将会丢失是否继续', updateContainerHelper: '容器编辑需要重建容器任何未持久化的数据将会丢失是否继续',
containerList: '容器列表', containerList: '容器列表',
@ -644,6 +648,7 @@ const message = {
imageLoadErr: '未检测到容器的镜像名称', imageLoadErr: '未检测到容器的镜像名称',
appHelper: '该容器来源于应用商店升级可能导致该服务不可用', appHelper: '该容器来源于应用商店升级可能导致该服务不可用',
resource: '资源',
input: '手动输入', input: '手动输入',
forcePull: '强制拉取镜像', forcePull: '强制拉取镜像',
forcePullHelper: '忽略服务器已存在的镜像重新拉取一次', forcePullHelper: '忽略服务器已存在的镜像重新拉取一次',

View File

@ -0,0 +1,110 @@
<template>
<div>
<el-dialog v-model="drawerVisible" :title="$t('container.createByCommand')" :back="handleClose" width="70%">
<el-form
@submit.prevent
ref="formRef"
:rules="rules"
:model="form"
label-position="top"
v-loading="loading"
>
<el-form-item prop="command">
<CodemirrorPro
:lineWrapping="true"
v-model="form.command"
:height="300"
:minHeight="50"
mode="shell"
placeholder="e.g. docker run -p 80:80 --name my-nginx nginx"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button :disabled="loading" @click="drawerVisible = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button :disabled="loading" type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
<TaskLog ref="taskLogRef" width="70%" />
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { ElForm } from 'element-plus';
import i18n from '@/lang';
import TaskLog from '@/components/task-log/index.vue';
import { createContainerByCommand } from '@/api/modules/container';
import { MsgSuccess } from '@/utils/message';
import { newUUID } from '@/utils/util';
const drawerVisible = ref<boolean>(false);
const emit = defineEmits<{ (e: 'search'): void }>();
const loading = ref(false);
const form = reactive({
command: '',
});
const taskLogRef = ref();
const acceptParams = (): void => {
form.command = '';
drawerVisible.value = true;
};
const formRef = ref<FormInstance>();
type FormInstance = InstanceType<typeof ElForm>;
const verifyCommand = (rule: any, value: any, callback: any) => {
if (!form.command || !form.command.startsWith('docker run')) {
callback(new Error(i18n.global.t('container.commandRule')));
return;
}
callback();
};
const rules = reactive({
command: [{ validator: verifyCommand, trigger: 'blur', required: true }],
});
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
ElMessageBox.confirm(i18n.global.t('container.commandHelper'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
loading.value = true;
let taskID = newUUID();
await createContainerByCommand(form.command, taskID)
.then(() => {
loading.value = false;
emit('search');
openTaskLog(taskID);
drawerVisible.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
});
});
};
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const handleClose = async () => {
drawerVisible.value = false;
emit('search');
};
defineExpose({
acceptParams,
});
</script>

View File

@ -1,6 +1,9 @@
<template> <template>
<div> <div>
<LayoutContent :title="isCreate ? $t('container.create') : $t('commons.button.edit') + ' - ' + form.name"> <LayoutContent
back-name="Container"
:title="isCreate ? $t('container.create') : $t('commons.button.edit') + ' - ' + form.name"
>
<template #prompt> <template #prompt>
<el-alert <el-alert
v-if="!isCreate && isFromApp(form)" v-if="!isCreate && isFromApp(form)"
@ -18,13 +21,20 @@
:rules="rules" :rules="rules"
label-width="80px" label-width="80px"
> >
<el-row> <el-row type="flex" justify="center" :gutter="20">
<el-col :span="1"><br /></el-col> <el-col :span="20">
<el-col :xs="24" :sm="20" :md="15" :lg="12" :xl="12"> <el-card>
<el-form-item class="mt-5" :label="$t('commons.table.name')" prop="name"> <el-button v-if="isCreate" type="primary" icon="EditPen" plain @click="openDialog()">
<el-input :disabled="isFromApp(form)" clearable v-model.trim="form.name" /> {{ $t('container.commandInput') }}
<div v-if="!isCreate && isFromApp(form)"> </el-button>
<span class="input-help"> <el-form-item class="mt-5" :label="$t('commons.table.name')" prop="name">
<el-input
:disabled="isFromApp(form)"
class="mini-form-item"
clearable
v-model.trim="form.name"
/>
<span class="input-help" v-if="!isCreate && isFromApp(form)">
{{ $t('container.containerFromAppHelper1') }} {{ $t('container.containerFromAppHelper1') }}
<el-button <el-button
style="margin-left: -5px" style="margin-left: -5px"
@ -37,36 +47,47 @@
{{ $t('firewall.quickJump') }} {{ $t('firewall.quickJump') }}
</el-button> </el-button>
</span> </span>
</div> </el-form-item>
</el-form-item> <el-form-item :label="$t('container.image')" prop="image">
<el-form-item :label="$t('container.image')" prop="image"> <el-checkbox v-model="form.imageInput" :label="$t('container.input')" />
<el-checkbox v-model="form.imageInput" :label="$t('container.input')" /> </el-form-item>
<el-select v-if="!form.imageInput" filterable v-model="form.image"> <el-form-item>
<el-option <el-select
v-for="(item, index) of images" class="mini-form-item"
:key="index" v-if="!form.imageInput"
:value="item.option" filterable
:label="item.option" v-model="form.image"
/> >
</el-select> <el-option
<el-input v-else v-model="form.image" /> v-for="(item, index) of images"
</el-form-item> :key="index"
<el-form-item prop="forcePull"> :value="item.option"
<el-checkbox v-model="form.forcePull"> :label="item.option"
{{ $t('container.forcePull') }} />
</el-checkbox> </el-select>
<span class="input-help">{{ $t('container.forcePullHelper') }}</span> <el-input class="mini-form-item" v-else v-model="form.image" />
</el-form-item> </el-form-item>
<el-form-item :label="$t('commons.table.port')"> <el-form-item prop="forcePull">
<el-radio-group v-model="form.publishAllPorts" class="ml-4"> <el-checkbox v-model="form.forcePull">
<el-radio :value="false">{{ $t('container.exposePort') }}</el-radio> {{ $t('container.forcePull') }}
<el-radio :value="true">{{ $t('container.exposeAll') }}</el-radio> </el-checkbox>
</el-radio-group> <span class="input-help">{{ $t('container.forcePullHelper') }}</span>
</el-form-item> </el-form-item>
<el-form-item v-if="!form.publishAllPorts">
<el-card class="widthClass"> <el-form-item prop="autoRemove">
<el-checkbox v-model="form.autoRemove">
{{ $t('container.autoRemove') }}
</el-checkbox>
</el-form-item>
<el-form-item :label="$t('commons.table.port')">
<el-radio-group v-model="form.publishAllPorts" class="ml-4">
<el-radio :value="false">{{ $t('container.exposePort') }}</el-radio>
<el-radio :value="true">{{ $t('container.exposeAll') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="!form.publishAllPorts">
<el-table v-if="form.exposedPorts.length !== 0" :data="form.exposedPorts"> <el-table v-if="form.exposedPorts.length !== 0" :data="form.exposedPorts">
<el-table-column :label="$t('container.server')" min-width="150"> <el-table-column :label="$t('container.server')" min-width="200">
<template #default="{ row }"> <template #default="{ row }">
<el-input <el-input
:placeholder="$t('container.serverExample')" :placeholder="$t('container.serverExample')"
@ -74,7 +95,7 @@
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('container.container')" min-width="80"> <el-table-column :label="$t('container.container')" min-width="120">
<template #default="{ row }"> <template #default="{ row }">
<el-input <el-input
:placeholder="$t('container.containerExample')" :placeholder="$t('container.containerExample')"
@ -82,19 +103,15 @@
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('commons.table.protocol')" min-width="50"> <el-table-column :label="$t('commons.table.protocol')" min-width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-select <el-radio-group v-model="row.protocol">
v-model="row.protocol" <el-radio value="tcp">tcp</el-radio>
style="width: 100%" <el-radio value="udp">udp</el-radio>
:placeholder="$t('container.serverExample')" </el-radio-group>
>
<el-option label="tcp" value="tcp" />
<el-option label="udp" value="udp" />
</el-select>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column min-width="35"> <el-table-column min-width="80">
<template #default="scope"> <template #default="scope">
<el-button link type="primary" @click="handlePortsDelete(scope.$index)"> <el-button link type="primary" @click="handlePortsDelete(scope.$index)">
{{ $t('commons.button.delete') }} {{ $t('commons.button.delete') }}
@ -103,55 +120,65 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-button class="ml-3 mt-2" @click="handlePortsAdd()"> <el-button class="ml-3" @click="handlePortsAdd()">
{{ $t('commons.button.add') }} {{ $t('commons.button.add') }}
</el-button> </el-button>
</el-card> </el-form-item>
</el-form-item> </el-card>
<el-form-item :label="$t('container.network')" prop="network">
<el-select v-model="form.network">
<el-option
v-for="(item, indexV) of networks"
:key="indexV"
:value="item.option"
:label="item.option"
/>
</el-select>
</el-form-item>
<el-form-item label="ipv4" prop="ipv4"> <el-tabs type="border-card" class="mt-5">
<el-input v-model="form.ipv4" :placeholder="$t('container.inputIpv4')" /> <el-tab-pane :label="$t('container.network')">
</el-form-item> <el-form-item :label="$t('container.network')" prop="network">
<el-form-item label="ipv6" prop="ipv6"> <el-select class="mini-form-item" v-model="form.network">
<el-input v-model="form.ipv6" :placeholder="$t('container.inputIpv6')" /> <el-option
</el-form-item> v-for="(item, indexV) of networks"
:key="indexV"
:value="item.option"
:label="item.option"
/>
</el-select>
</el-form-item>
<el-form-item label="ipv4" prop="ipv4">
<el-input
class="mini-form-item"
v-model="form.ipv4"
:placeholder="$t('container.inputIpv4')"
/>
</el-form-item>
<el-form-item label="ipv6" prop="ipv6">
<el-input
class="mini-form-item"
v-model="form.ipv6"
:placeholder="$t('container.inputIpv6')"
/>
</el-form-item>
</el-tab-pane>
<el-form-item :label="$t('container.mount')"> <el-tab-pane :label="$t('container.mount')">
<div v-for="(row, index) in form.volumes" :key="index" style="width: 100%"> <el-form-item>
<el-card class="mt-1"> <el-table v-if="form.volumes.length !== 0" :data="form.volumes">
<el-radio-group v-model="row.type"> <el-table-column :label="$t('container.server')" min-width="120">
<el-radio-button value="volume"> <template #default="{ row }">
{{ $t('container.volumeOption') }} <el-radio-group v-model="row.type">
</el-radio-button> <el-radio-button value="volume">
<el-radio-button value="bind"> {{ $t('container.volumeOption') }}
{{ $t('container.hostOption') }} </el-radio-button>
</el-radio-button> <el-radio-button value="bind">
</el-radio-group> {{ $t('container.hostOption') }}
<el-button </el-radio-button>
class="float-right mt-3" </el-radio-group>
link </template>
type="primary" </el-table-column>
@click="handleVolumesDelete(index)" <el-table-column
> :label="$t('container.volumeOption') + '/' + $t('container.hostOption')"
{{ $t('commons.button.delete') }} min-width="200"
</el-button> >
<el-row class="mt-4" :gutter="5"> <template #default="{ row }">
<el-col :span="10"> <el-select
<el-form-item v-if="row.type === 'volume'"
v-if="row.type === 'volume'" filterable
:label="$t('container.volumeOption')" v-model="row.sourceDir"
> >
<el-select filterable v-model="row.sourceDir">
<div v-for="(item, indexV) of volumes" :key="indexV"> <div v-for="(item, indexV) of volumes" :key="indexV">
<el-tooltip <el-tooltip
:hide-after="20" :hide-after="20"
@ -165,121 +192,145 @@
</el-tooltip> </el-tooltip>
</div> </div>
</el-select> </el-select>
</el-form-item> <el-input v-else v-model="row.sourceDir" />
<el-form-item v-else :label="$t('container.hostOption')"> </template>
<el-input v-model="row.sourceDir" /> </el-table-column>
</el-form-item> <el-table-column :label="$t('container.mode')" min-width="120">
</el-col> <template #default="{ row }">
<el-col :span="5"> <el-radio-group v-model="row.mode">
<el-form-item :label="$t('container.mode')"> <el-radio value="rw">{{ $t('container.modeRW') }}</el-radio>
<el-select class="widthClass" filterable v-model="row.mode"> <el-radio value="ro">{{ $t('container.modeR') }}</el-radio>
<el-option value="rw" :label="$t('container.modeRW')" /> </el-radio-group>
<el-option value="ro" :label="$t('container.modeR')" /> </template>
</el-select> </el-table-column>
</el-form-item> <el-table-column :label="$t('container.containerDir')" min-width="200">
</el-col> <template #default="{ row }">
<el-col :span="9">
<el-form-item :label="$t('container.containerDir')">
<el-input v-model="row.containerDir" /> <el-input v-model="row.containerDir" />
</el-form-item> </template>
</el-col> </el-table-column>
</el-row> <el-table-column min-width="80">
</el-card> <template #default="scope">
</div> <el-button
<el-button @click="handleVolumesAdd()"> link
{{ $t('commons.button.add') }} type="primary"
@click="handleVolumesDelete(scope.$index)"
>
{{ $t('commons.button.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<el-button @click="handleVolumesAdd()">
{{ $t('commons.button.add') }}
</el-button>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="Command">
<el-form-item label="Command" prop="cmdStr">
<el-input v-model="form.cmdStr" :placeholder="$t('container.cmdHelper')" />
</el-form-item>
<el-form-item label="Entrypoint" prop="entrypointStr">
<el-input
v-model="form.entrypointStr"
:placeholder="$t('container.entrypointHelper')"
/>
</el-form-item>
<el-form-item :label="$t('container.console')">
<el-checkbox v-model="form.tty">{{ $t('container.tty') }}</el-checkbox>
<el-checkbox v-model="form.openStdin">
{{ $t('container.openStdin') }}
</el-checkbox>
</el-form-item>
</el-tab-pane>
<el-tab-pane :label="$t('container.resource')">
<el-form-item :label="$t('container.cpuShare')" prop="cpuShares">
<el-input class="mini-form-item" v-model.number="form.cpuShares" />
<span class="input-help">{{ $t('container.cpuShareHelper') }}</span>
</el-form-item>
<el-form-item
:label="$t('container.cpuQuota')"
prop="nanoCPUs"
:rules="checkFloatNumberRange(0, Number(limits.cpu))"
>
<el-input class="mini-form-item" v-model="form.nanoCPUs">
<template #append>
<div style="width: 35px">{{ $t('commons.units.core') }}</div>
</template>
</el-input>
<span class="input-help">
{{ $t('container.limitHelper', [limits.cpu])
}}{{ $t('commons.units.core') }}
</span>
</el-form-item>
<el-form-item
:label="$t('container.memoryLimit')"
prop="memory"
:rules="checkFloatNumberRange(0, Number(limits.memory))"
>
<el-input class="mini-form-item" v-model="form.memory">
<template #append><div style="width: 35px">MB</div></template>
</el-input>
<span class="input-help">
{{ $t('container.limitHelper', [limits.memory]) }}MB
</span>
</el-form-item>
<el-form-item>
<el-checkbox v-model="form.privileged">
{{ $t('container.privileged') }}
</el-checkbox>
<span class="input-help">{{ $t('container.privilegedHelper') }}</span>
</el-form-item>
</el-tab-pane>
<el-tab-pane :label="$t('container.tag') + ' & ' + $t('container.env')">
<el-form-item :label="$t('container.tag')" prop="labelsStr">
<el-input
type="textarea"
:placeholder="$t('container.tagHelper')"
:rows="3"
v-model="form.labelsStr"
/>
</el-form-item>
<el-form-item :label="$t('container.env')" prop="envStr">
<el-input
type="textarea"
:placeholder="$t('container.tagHelper')"
:rows="3"
v-model="form.envStr"
/>
</el-form-item>
</el-tab-pane>
<el-tab-pane :label="$t('container.restartPolicy')">
<el-form-item prop="restartPolicy">
<el-radio-group v-model="form.restartPolicy">
<el-radio value="no">{{ $t('container.no') }}</el-radio>
<el-radio value="always">{{ $t('container.always') }}</el-radio>
<el-radio value="on-failure">{{ $t('container.onFailure') }}</el-radio>
<el-radio value="unless-stopped">
{{ $t('container.unlessStopped') }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-tab-pane>
</el-tabs>
<el-form-item class="mt-5">
<el-button :disabled="loading" @click="goBack">
{{ $t('commons.button.back') }}
</el-button>
<el-button :disabled="loading" type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button> </el-button>
</el-form-item> </el-form-item>
<el-form-item label="Command" prop="cmdStr">
<el-input v-model="form.cmdStr" :placeholder="$t('container.cmdHelper')" />
</el-form-item>
<el-form-item label="Entrypoint" prop="entrypointStr">
<el-input
v-model="form.entrypointStr"
:placeholder="$t('container.entrypointHelper')"
/>
</el-form-item>
<el-form-item prop="autoRemove">
<el-checkbox v-model="form.autoRemove">
{{ $t('container.autoRemove') }}
</el-checkbox>
</el-form-item>
<el-form-item>
<el-checkbox v-model="form.privileged">
{{ $t('container.privileged') }}
</el-checkbox>
<span class="input-help">{{ $t('container.privilegedHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('container.console')">
<el-checkbox v-model="form.tty">{{ $t('container.tty') }}</el-checkbox>
<el-checkbox v-model="form.openStdin">
{{ $t('container.openStdin') }}
</el-checkbox>
</el-form-item>
<el-form-item :label="$t('container.restartPolicy')" prop="restartPolicy">
<el-radio-group v-model="form.restartPolicy">
<el-radio value="no">{{ $t('container.no') }}</el-radio>
<el-radio value="always">{{ $t('container.always') }}</el-radio>
<el-radio value="on-failure">{{ $t('container.onFailure') }}</el-radio>
<el-radio value="unless-stopped">{{ $t('container.unlessStopped') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('container.cpuShare')" prop="cpuShares">
<el-input class="mini-form-item" v-model.number="form.cpuShares" />
<span class="input-help">{{ $t('container.cpuShareHelper') }}</span>
</el-form-item>
<el-form-item
:label="$t('container.cpuQuota')"
prop="nanoCPUs"
:rules="checkFloatNumberRange(0, Number(limits.cpu))"
>
<el-input class="mini-form-item" v-model="form.nanoCPUs">
<template #append>
<div style="width: 35px">{{ $t('commons.units.core') }}</div>
</template>
</el-input>
<span class="input-help">
{{ $t('container.limitHelper', [limits.cpu]) }}{{ $t('commons.units.core') }}
</span>
</el-form-item>
<el-form-item
:label="$t('container.memoryLimit')"
prop="memory"
:rules="checkFloatNumberRange(0, Number(limits.memory))"
>
<el-input class="mini-form-item" v-model="form.memory">
<template #append><div style="width: 35px">MB</div></template>
</el-input>
<span class="input-help">{{ $t('container.limitHelper', [limits.memory]) }}MB</span>
</el-form-item>
<el-form-item :label="$t('container.tag')" prop="labelsStr">
<el-input
type="textarea"
:placeholder="$t('container.tagHelper')"
:rows="3"
v-model="form.labelsStr"
/>
</el-form-item>
<el-form-item :label="$t('container.env')" prop="envStr">
<el-input
type="textarea"
:placeholder="$t('container.tagHelper')"
:rows="3"
v-model="form.envStr"
/>
</el-form-item>
<el-button :disabled="loading" @click="goBack">
{{ $t('commons.button.back') }}
</el-button>
<el-button :disabled="loading" type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</el-col> </el-col>
</el-row> </el-row>
</el-form> </el-form>
</template> </template>
</LayoutContent> </LayoutContent>
<Command ref="commandRef" />
</div> </div>
</template> </template>
@ -288,6 +339,7 @@ import { reactive, ref } from 'vue';
import { Rules, checkFloatNumberRange, checkNumberRange } from '@/global/form-rules'; import { Rules, checkFloatNumberRange, checkNumberRange } from '@/global/form-rules';
import i18n from '@/lang'; import i18n from '@/lang';
import { ElForm, ElMessageBox } from 'element-plus'; import { ElForm, ElMessageBox } from 'element-plus';
import Command from '@/views/container/container/command/index.vue';
import { import {
listImage, listImage,
listVolume, listVolume,
@ -389,6 +441,7 @@ const search = async () => {
loadNetworkOptions(); loadNetworkOptions();
}; };
const commandRef = ref();
const images = ref(); const images = ref();
const volumes = ref(); const volumes = ref();
const networks = ref(); const networks = ref();
@ -412,6 +465,10 @@ const goBack = () => {
router.push({ name: 'Container' }); router.push({ name: 'Container' });
}; };
const openDialog = () => {
commandRef.value.acceptParams();
};
const handlePortsAdd = () => { const handlePortsAdd = () => {
let item = { let item = {
host: '', host: '',

View File

@ -19,10 +19,12 @@
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-checkbox v-model="form.deleteTag"> <div class="w-full">
{{ $t('container.imageTagDeleteHelper') }} <el-checkbox v-model="form.deleteTag">
</el-checkbox> {{ $t('container.imageTagDeleteHelper') }}
<el-checkbox-group class="ml-5" v-if="form.deleteTag" v-model="form.deleteTags"> </el-checkbox>
</div>
<el-checkbox-group v-if="form.deleteTag" v-model="form.deleteTags">
<el-checkbox v-for="item in tags" :key="item" :value="item" :label="item" /> <el-checkbox v-for="item in tags" :key="item" :value="item" :label="item" />
</el-checkbox-group> </el-checkbox-group>
</el-form-item> </el-form-item>