feat: Configure restart policy for application installation support (#9953)

Refs https://github.com/1Panel-dev/1Panel/issues/9895
This commit is contained in:
CityFun 2025-08-11 18:16:44 +08:00 committed by GitHub
parent b735178fcd
commit f8f47b51e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 550 additions and 466 deletions

View file

@ -40,6 +40,7 @@ type AppContainerConfig struct {
WebUI string `json:"webUI"`
Type string `json:"type"`
SpecifyIP string `json:"specifyIP"`
RestartPolicy string `json:"restartPolicy" validate:"omitempty,oneof=always unless-stopped no on-failure"`
}
type AppInstalledSearch struct {

View file

@ -788,6 +788,7 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
}
res.AppContainerConfig = config
res.HostMode = isHostModel(install.DockerCompose)
res.RestartPolicy = getRestartPolicy(install.DockerCompose)
res.WebUI = install.WebUI
res.Type = install.App.Type
return &res, nil

View file

@ -357,7 +357,7 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
if err != nil {
return err
}
images, err := composeV2.GetDockerComposeImagesV2(content, []byte(install.DockerCompose))
images, err := composeV2.GetImagesFromDockerCompose(content, []byte(install.DockerCompose))
if err != nil {
return err
}
@ -540,6 +540,9 @@ func handleUpgradeCompose(install model.AppInstall, detail model.AppDetail) (map
if oldServiceValue["deploy"] != nil {
serviceValue["deploy"] = oldServiceValue["deploy"]
}
if oldServiceValue["restart"] != nil {
serviceValue["restart"] = oldServiceValue["restart"]
}
servicesMap[install.ServiceName] = serviceValue
composeMap["services"] = servicesMap
return composeMap, nil
@ -659,7 +662,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if req.DockerCompose != "" {
composeContent = []byte(req.DockerCompose)
}
images, err := composeV2.GetDockerComposeImagesV2(content, composeContent)
images, err := composeV2.GetImagesFromDockerCompose(content, composeContent)
if err != nil {
return err
}
@ -1658,6 +1661,14 @@ func addDockerComposeCommonParam(composeMap map[string]interface{}, serviceName
deploy["resources"] = resource
serviceValue["deploy"] = deploy
if req.RestartPolicy != "" {
if req.RestartPolicy == "on-failure" {
serviceValue["restart"] = "on-failure:5"
} else {
serviceValue["restart"] = req.RestartPolicy
}
}
ports, ok := serviceValue["ports"].([]interface{})
if ok {
for i, port := range ports {
@ -1760,6 +1771,17 @@ func isHostModel(dockerCompose string) bool {
return false
}
func getRestartPolicy(yml string) string {
var project docker.ComposeProject
if err := yaml.Unmarshal([]byte(yml), &project); err != nil {
return ""
}
for _, service := range project.Services {
return service.Restart
}
return ""
}
func getMajorVersion(version string) string {
parts := strings.Split(version, ".")
if len(parts) >= 2 {

View file

@ -58,6 +58,7 @@ type Service struct {
Image string `yaml:"image"`
Environment Environment `yaml:"environment"`
Volumes []string `json:"volumes"`
Restart string `json:"restart"`
}
type Environment struct {
@ -92,69 +93,6 @@ func (e *Environment) UnmarshalYAML(value *yaml.Node) error {
return nil
}
func replaceEnvVariables(input string, envVars map[string]string) string {
for key, value := range envVars {
placeholder := fmt.Sprintf("${%s}", key)
input = strings.ReplaceAll(input, placeholder, value)
}
return input
}
func GetDockerComposeImagesV2(env, yml []byte) ([]string, error) {
var (
compose ComposeProject
err error
images []string
)
err = yaml.Unmarshal(yml, &compose)
if err != nil {
return nil, err
}
envMap, err := godotenv.UnmarshalBytes(env)
if err != nil {
return nil, err
}
for _, service := range compose.Services {
image := replaceEnvVariables(service.Image, envMap)
images = append(images, image)
}
return images, nil
}
func GetDockerComposeImages(projectName string, env, yml []byte) ([]string, error) {
var (
configFiles []types.ConfigFile
images []string
imagesMap = make(map[string]struct{})
)
configFiles = append(configFiles, types.ConfigFile{
Filename: "docker-compose.yml",
Content: yml},
)
envMap, err := godotenv.UnmarshalBytes(env)
if err != nil {
return nil, err
}
details := types.ConfigDetails{
ConfigFiles: configFiles,
Environment: envMap,
}
project, err := loader.LoadWithContext(context.Background(), details, func(options *loader.Options) {
options.SetProjectName(projectName, true)
options.ResolvePaths = true
})
if err != nil {
return nil, err
}
for _, service := range project.AllServices() {
imagesMap[service.Image] = struct{}{}
}
for image := range imagesMap {
images = append(images, image)
}
return images, nil
}
func GetImagesFromDockerCompose(env, yml []byte) ([]string, error) {
envVars, err := loadEnvFile(env)
if err != nil {

View file

@ -276,6 +276,7 @@ export namespace App {
type: string;
webUI: string;
specifyIP: string;
restartPolicy: string;
}
export interface IgnoredApp {

View file

@ -15,12 +15,9 @@ export const searchApp = (req: App.AppReq) => {
return http.post<App.AppResPage>('apps/search', req);
};
export const getAppByKey = (key: string) => {
return http.get<App.AppDTO>('apps/' + key);
};
export const getAppByKeyWithNode = (key: string, node: string) => {
return http.get<App.AppDTO>('apps/' + key + `?operateNode=${node}`);
export const getAppByKey = (key: string, node?: string) => {
const params = node ? `?operateNode=${node}` : '';
return http.get<App.AppDTO>('apps/' + key + `${params}`);
};
export const getAppTags = () => {

View file

@ -0,0 +1,400 @@
<template>
<div>
<el-alert
:title="$t('app.hostModeHelper')"
class="common-prompt"
:closable="false"
type="warning"
v-if="isHostMode"
/>
<el-alert
:title="$t('app.memoryRequiredHelper', [computeSizeFromMB(memoryRequired)])"
class="common-prompt"
:closable="false"
type="warning"
v-if="memoryRequired > 0"
/>
<el-form
v-loading="loading"
@submit.prevent
ref="formRef"
label-position="top"
:model="formData"
label-width="150px"
:rules="formRules"
:validate-on-rule-change="false"
>
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model.trim="formData.name"></el-input>
</el-form-item>
<el-form-item :label="$t('app.version')" prop="version">
<el-select v-model="formData.version" @change="handleVersionChange">
<el-option
v-for="(version, index) in appVersions"
:key="index"
:label="version"
:value="version"
></el-option>
</el-select>
</el-form-item>
<Params
:key="paramKey"
v-if="showParams"
v-model:form="formData.params"
v-model:params="installParams"
v-model:rules="formRules.params"
:propStart="'params.'"
/>
<el-form-item prop="advanced">
<el-checkbox v-model="formData.advanced" :label="$t('app.advanced')" size="large" />
</el-form-item>
<div v-if="formData.advanced">
<el-form-item :label="$t('app.containerName')" prop="containerName">
<el-input v-model.trim="formData.containerName" :placeholder="$t('app.containerNameHelper')" />
</el-form-item>
<el-form-item prop="allowPort" v-if="!isHostMode">
<el-checkbox v-model="formData.allowPort" :label="$t('app.allowPort')" size="large" />
<span class="input-help">{{ $t('app.allowPortHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('app.specifyIP')" v-if="formData.allowPort" prop="specifyIP">
<el-input v-model="formData.specifyIP" />
<span class="input-help">{{ $t('app.specifyIPHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('container.restartPolicy')" prop="restartPolicy">
<el-select v-model="formData.restartPolicy" class="p-w-300">
<el-option :label="$t('container.no')" value="no"></el-option>
<el-option :label="$t('container.always')" value="always"></el-option>
<el-option :label="$t('container.onFailure')" value="on-failure"></el-option>
<el-option :label="$t('container.unlessStopped')" value="unless-stopped"></el-option>
</el-select>
</el-form-item>
<el-form-item
:label="$t('container.cpuQuota')"
prop="cpuQuota"
:rules="checkNumberRange(0, limits.cpu)"
>
<el-input type="number" class="!w-2/5" v-model.number="formData.cpuQuota" maxlength="5">
<template #append>{{ $t('app.cpuCore') }}</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="memoryLimit"
:rules="checkNumberRange(0, limits.memory)"
>
<el-input class="!w-2/5" v-model.number="formData.memoryLimit" maxlength="10">
<template #append>
<el-select
v-model="formData.memoryUnit"
placeholder="Select"
class="p-w-100"
@change="changeUnit"
>
<el-option label="MB" value="M" />
<el-option label="GB" value="G" />
</el-select>
</template>
</el-input>
<span class="input-help">
{{ $t('container.limitHelper', [limits.memory]) }}{{ formData.memoryUnit }}B
</span>
</el-form-item>
<el-form-item pro="gpuConfig" v-if="gpuSupport">
<el-checkbox v-model="formData.gpuConfig" :label="$t('app.gpuConfig')" size="large" />
<span class="input-help">{{ $t('app.gpuConfigHelper') }}</span>
</el-form-item>
<el-form-item pro="pullImage">
<el-checkbox v-model="formData.pullImage" :label="$t('app.pullImage')" size="large" />
<span class="input-help">{{ $t('app.pullImageHelper') }}</span>
</el-form-item>
<el-form-item prop="editCompose">
<el-checkbox v-model="formData.editCompose" :label="$t('app.editCompose')" size="large" />
<span class="input-help">{{ $t('app.editComposeHelper') }}</span>
</el-form-item>
<div v-if="formData.editCompose">
<CodemirrorPro v-model="formData.dockerCompose" mode="yaml" />
</div>
</div>
</el-form>
</div>
</template>
<script lang="ts" setup name="AppInstallForm">
import { App } from '@/api/interface/app';
import { getAppByKey, getAppDetail, getAppInstalledByID } from '@/api/modules/app';
import { Rules, checkNumberRange } from '@/global/form-rules';
import { FormInstance, FormRules } from 'element-plus';
import { reactive, ref, watch } from 'vue';
import Params from '../params/index.vue';
import { Container } from '@/api/interface/container';
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import { computeSizeFromMB } from '@/utils/util';
import { loadResourceLimit } from '@/api/modules/container';
interface ClusterProps {
key: string;
node: string;
masterNode: string;
appInstallID: number;
masterVersion: string;
masterNodeAddr: string;
role: string;
}
interface Props {
loading?: boolean;
modelValue?: any;
}
const limits = ref<Container.ResourceLimit>({
cpu: null as number,
memory: null as number,
});
const props = withDefaults(defineProps<Props>(), {
loading: false,
});
interface Emits {
(e: 'update:modelValue', value: any): void;
}
const emit = defineEmits<Emits>();
const formRef = ref<FormInstance>();
const paramKey = ref(1);
const isHostMode = ref(false);
const memoryRequired = ref(0);
const gpuSupport = ref(false);
const installParams = ref<App.AppParams>();
const oldMemory = ref<number>(0);
const showParams = ref(false);
const currentApp = ref<any>({});
const appVersions = ref<string[]>([]);
const operateNode = ref();
const env = ref();
const masterNodeAddr = ref();
const formRules = ref<FormRules>({
name: [Rules.appName],
params: [],
version: [Rules.requiredSelect],
containerName: [Rules.containerName],
cpuQuota: [Rules.requiredInput, checkNumberRange(0, 99999)],
memoryLimit: [Rules.requiredInput, checkNumberRange(0, 9999999999)],
specifyIP: [Rules.ipv4orV6],
restartPolicy: [Rules.requiredSelect],
});
const initFormData = () => ({
appDetailId: 0,
params: {},
name: '',
advanced: true,
cpuQuota: 0,
memoryLimit: 0,
memoryUnit: 'M',
containerName: '',
allowPort: false,
editCompose: false,
dockerCompose: '',
version: '',
appID: '',
pullImage: true,
taskID: '',
gpuConfig: false,
specifyIP: '',
restartPolicy: 'always',
});
const formData = reactive(props.modelValue || initFormData());
watch(
formData,
(newVal) => {
emit('update:modelValue', newVal);
},
{ deep: true },
);
watch(
() => props.modelValue,
(newVal) => {
if (newVal) {
Object.assign(formData, newVal);
}
},
{ immediate: true, deep: true },
);
const changeUnit = () => {
if (formData.memoryUnit == 'M') {
limits.value.memory = oldMemory.value;
} else {
limits.value.memory = Number((oldMemory.value / 1024).toFixed(2));
}
};
const handleVersionChange = async (version: string) => {
await getVersionDetail(version);
};
const getVersionDetail = async (version: string) => {
try {
const res = await getAppDetail(currentApp.value.id, version, 'app', operateNode.value);
formData.appDetailId = res.data.id;
formData.dockerCompose = res.data.dockerCompose;
isHostMode.value = res.data.hostMode;
if (env.value) {
installParams.value = addMasterParams(res.data.params);
} else {
installParams.value = res.data.params;
}
paramKey.value++;
memoryRequired.value = res.data.memoryRequired;
gpuSupport.value = res.data.gpuSupport;
showParams.value = true;
} catch (error) {}
};
const initForm = async (appKey: string) => {
formData.name = appKey;
const res = await getAppByKey(appKey);
currentApp.value = res.data;
appVersions.value = currentApp.value.versions;
if (appVersions.value.length > 0) {
const defaultVersion = appVersions.value[0];
formData.version = defaultVersion;
getVersionDetail(defaultVersion);
}
};
const getMasterAppInstall = async (appInstallID: number, masterNode: string) => {
try {
const res = await getAppInstalledByID(appInstallID, masterNode);
env.value = res.data.env;
} catch (error) {}
};
const addMasterParams = (appParams: App.AppParams) => {
for (const key in appParams.formFields) {
const field = appParams.formFields[key];
if (field?.envKey === 'MASTER_ROOT_PASSWORD') {
field.default = env.value['PANEL_DB_ROOT_PASSWORD']
? env.value['PANEL_DB_ROOT_PASSWORD']
: env.value['PANEL_REDIS_ROOT_PASSWORD'];
}
if (field?.envKey === 'PANEL_DB_ROOT_PASSWORD') {
field.default = env.value['PANEL_DB_ROOT_PASSWORD'];
}
if (field?.envKey === 'REPLICATION_USER') {
field.default = env.value['REPLICATION_USER'];
}
if (field?.envKey === 'REPLICATION_PASSWORD') {
field.default = env.value['REPLICATION_PASSWORD'];
}
if (field?.envKey === 'MASTER_PORT') {
field.default = env.value['PANEL_APP_PORT_HTTP'];
}
if (field?.envKey === 'MASTER_HOST' && masterNodeAddr.value != '127.0.0.1') {
field.default = masterNodeAddr.value;
}
}
return appParams;
};
const initClusterForm = async (props: ClusterProps) => {
if (props.appInstallID && props.masterNode) {
getMasterAppInstall(props.appInstallID, props.masterNode);
}
masterNodeAddr.value = props.masterNodeAddr;
operateNode.value = props.node;
const res = await getAppByKey(props.key, props.node);
currentApp.value = res.data;
appVersions.value = currentApp.value.versions;
if (appVersions.value.length > 0) {
appVersions.value = appVersions.value.filter((v: string) => {
return v.includes(props.role) && v.includes(props.masterVersion);
});
const defaultVersion = appVersions.value[0];
formData.version = defaultVersion;
getVersionDetail(defaultVersion);
}
formData.name = props.key + '-' + props.role;
};
const resetForm = () => {
if (formRef.value) {
formRef.value.clearValidate();
formRef.value.resetFields();
}
Object.assign(formData, initFormData());
isHostMode.value = false;
memoryRequired.value = 0;
gpuSupport.value = false;
showParams.value = false;
};
const validate = async (): Promise<boolean> => {
if (!formRef.value) return false;
try {
const isValid = await formRef.value.validate();
return isValid;
} catch (error) {
return false;
}
};
const clearValidate = () => {
if (formRef.value) {
formRef.value.clearValidate();
}
};
const getFormData = () => {
return { ...formData };
};
const setFormData = (data: any) => {
Object.assign(formData, data);
};
const loadLimit = async () => {
const res = await loadResourceLimit();
limits.value = res.data;
limits.value.memory = Number((limits.value.memory / 1024 / 1024).toFixed(2));
oldMemory.value = limits.value.memory;
};
onMounted(() => {
loadLimit();
});
defineExpose({
formRef,
formData,
initForm,
resetForm,
validate,
clearValidate,
getFormData,
setFormData,
isHostMode: () => isHostMode.value,
initClusterForm,
});
</script>

View file

@ -1,120 +1,12 @@
<template>
<DrawerPro v-model="open" :header="$t('commons.button.install')" @close="handleClose" size="large">
<el-alert
:title="$t('app.hostModeHelper')"
class="common-prompt"
:closable="false"
type="warning"
v-if="isHostMode"
/>
<el-alert
:title="$t('app.memoryRequiredHelper', [computeSizeFromMB(memoryRequired)])"
class="common-prompt"
:closable="false"
type="warning"
v-if="memoryRequired > 0"
/>
<el-form
v-loading="loading"
@submit.prevent
ref="paramForm"
label-position="top"
:model="req"
label-width="150px"
:rules="rules"
:validate-on-rule-change="false"
>
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model.trim="req.name"></el-input>
</el-form-item>
<el-form-item :label="$t('app.version')" prop="version">
<el-select v-model="req.version" @change="getDetail(req.version)">
<el-option
v-for="(version, index) in appVersions"
:key="index"
:label="version"
:value="version"
></el-option>
</el-select>
</el-form-item>
<Params
:key="paramKey"
v-if="open"
v-model:form="req.params"
v-model:params="installData.params"
v-model:rules="rules.params"
:propStart="'params.'"
></Params>
<el-form-item prop="advanced">
<el-checkbox v-model="req.advanced" :label="$t('app.advanced')" size="large" />
</el-form-item>
<div v-if="req.advanced">
<el-form-item :label="$t('app.containerName')" prop="containerName">
<el-input v-model.trim="req.containerName" :placeholder="$t('app.containerNameHelper')"></el-input>
</el-form-item>
<el-form-item prop="allowPort" v-if="!isHostMode">
<el-checkbox v-model="req.allowPort" :label="$t('app.allowPort')" size="large" />
<span class="input-help">{{ $t('app.allowPortHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('app.specifyIP')" v-if="req.allowPort" prop="specifyIP">
<el-input v-model="req.specifyIP"></el-input>
<span class="input-help">{{ $t('app.specifyIPHelper') }}</span>
</el-form-item>
<el-form-item
:label="$t('container.cpuQuota')"
prop="cpuQuota"
:rules="checkNumberRange(0, limits.cpu)"
>
<el-input type="number" style="width: 40%" v-model.number="req.cpuQuota" maxlength="5">
<template #append>{{ $t('app.cpuCore') }}</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="memoryLimit"
:rules="checkNumberRange(0, limits.memory)"
>
<el-input style="width: 40%" v-model.number="req.memoryLimit" maxlength="10">
<template #append>
<el-select
v-model="req.memoryUnit"
placeholder="Select"
style="width: 85px"
@change="changeUnit"
>
<el-option label="MB" value="M" />
<el-option label="GB" value="G" />
</el-select>
</template>
</el-input>
<span class="input-help">
{{ $t('container.limitHelper', [limits.memory]) }}{{ req.memoryUnit }}B
</span>
</el-form-item>
<el-form-item pro="gpuConfig" v-if="gpuSupport">
<el-checkbox v-model="req.gpuConfig" :label="$t('app.gpuConfig')" size="large" />
<span class="input-help">{{ $t('app.gpuConfigHelper') }}</span>
</el-form-item>
<el-form-item pro="pullImage">
<el-checkbox v-model="req.pullImage" :label="$t('app.pullImage')" size="large" />
<span class="input-help">{{ $t('app.pullImageHelper') }}</span>
</el-form-item>
<el-form-item prop="editCompose">
<el-checkbox v-model="req.editCompose" :label="$t('app.editCompose')" size="large" />
<span class="input-help">{{ $t('app.editComposeHelper') }}</span>
</el-form-item>
<div v-if="req.editCompose">
<CodemirrorPro v-model="req.dockerCompose" mode="yaml"></CodemirrorPro>
</div>
</div>
</el-form>
<AppInstallForm ref="installFormRef" v-model="formData" :loading="loading" />
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(paramForm)" :disabled="loading">
<el-button @click="handleClose" :disabled="loading">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button type="primary" @click="handleSubmit" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
@ -123,49 +15,26 @@
<TaskLog ref="taskLogRef" />
</template>
<script lang="ts" setup name="appInstall">
import { App } from '@/api/interface/app';
import { getAppByKey, getAppDetail, installApp } from '@/api/modules/app';
import { Rules, checkNumberRange } from '@/global/form-rules';
import { FormInstance, FormRules } from 'element-plus';
import { onMounted, reactive, ref } from 'vue';
<script lang="ts" setup name="AppInstallPage">
import { ref, reactive } from 'vue';
import { useRouter } from 'vue-router';
import Params from '../params/index.vue';
import i18n from '@/lang';
import AppInstallForm from '@/views/app-store/detail/form/index.vue';
import { installApp } from '@/api/modules/app';
import { MsgError } from '@/utils/message';
import { Container } from '@/api/interface/container';
import { loadResourceLimit } from '@/api/modules/container';
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import TaskLog from '@/components/log/task/index.vue';
import { newUUID } from '@/utils/util';
import { computeSizeFromMB } from '@/utils/util';
import { routerToName } from '@/utils/router';
interface InstallRrops {
params?: App.AppParams;
app: any;
}
const installData = ref<InstallRrops>({
app: {},
});
import TaskLog from '@/components/log/task/index.vue';
import i18n from '@/lang';
const router = useRouter();
const open = ref(false);
const rules = ref<FormRules>({
name: [Rules.appName],
params: [],
version: [Rules.requiredSelect],
containerName: [Rules.containerName],
cpuQuota: [Rules.requiredInput, checkNumberRange(0, 99999)],
memoryLimit: [Rules.requiredInput, checkNumberRange(0, 9999999999)],
specifyIP: [Rules.ipv4orV6],
});
const loading = ref(false);
const paramForm = ref<FormInstance>();
const form = ref<{ [key: string]: any }>({});
const initData = () => ({
const installFormRef = ref<InstanceType<typeof AppInstallForm>>();
const taskLogRef = ref();
const formData = reactive({
appDetailId: 0,
params: form.value,
params: {},
name: '',
advanced: true,
cpuQuota: 0,
@ -182,139 +51,73 @@ const initData = () => ({
gpuConfig: false,
specifyIP: '',
});
const req = reactive(initData());
const limits = ref<Container.ResourceLimit>({
cpu: null as number,
memory: null as number,
});
const oldMemory = ref<number>(0);
const appVersions = ref<string[]>([]);
const handleClose = () => {
open.value = false;
resetForm();
installFormRef.value?.resetForm();
if (router.currentRoute.value.query.install) {
routerToName('AppAll');
}
};
const paramKey = ref(1);
const isHostMode = ref(false);
const taskLogRef = ref();
const memoryRequired = ref(0);
const gpuSupport = ref(false);
const changeUnit = () => {
if (req.memoryUnit == 'M') {
limits.value.memory = oldMemory.value;
const handleSubmit = async () => {
const isValid = await installFormRef.value?.validate();
if (!isValid) return;
const submitData = installFormRef.value?.getFormData();
if (submitData.editCompose && submitData.dockerCompose === '') {
MsgError(i18n.global.t('app.composeNullErr'));
return;
}
if (submitData.cpuQuota < 0) {
submitData.cpuQuota = 0;
}
if (submitData.memoryLimit < 0) {
submitData.memoryLimit = 0;
}
const isHostMode = installFormRef.value?.isHostMode();
if (!isHostMode && !submitData.allowPort) {
ElMessageBox.confirm(i18n.global.t('app.installWarn'), i18n.global.t('app.checkTitle'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
await install(submitData);
});
} else {
limits.value.memory = Number((oldMemory.value / 1024).toFixed(2));
await install(submitData);
}
};
const resetForm = () => {
if (paramForm.value) {
paramForm.value.clearValidate();
paramForm.value.resetFields();
}
isHostMode.value = false;
Object.assign(req, initData());
};
const acceptParams = async (props: InstallRrops) => {
resetForm();
if (props.app.versions != undefined) {
installData.value = props;
} else {
const res = await getAppByKey(props.app.key);
installData.value.app = res.data;
}
const app = installData.value.app;
appVersions.value = app.versions;
if (appVersions.value.length > 0) {
req.version = appVersions.value[0];
getDetail(appVersions.value[0]);
}
req.name = props.app.key;
open.value = true;
};
const getDetail = async (version: string) => {
const install = async (submitData: any) => {
loading.value = true;
const taskID = newUUID();
submitData.taskID = taskID;
try {
const res = await getAppDetail(installData.value.app.id, version, 'app');
req.appDetailId = res.data.id;
req.dockerCompose = res.data.dockerCompose;
isHostMode.value = res.data.hostMode;
installData.value.params = res.data.params;
paramKey.value++;
memoryRequired.value = res.data.memoryRequired;
gpuSupport.value = res.data.gpuSupport;
await installApp(submitData);
handleClose();
openTaskLog(taskID);
} catch (error) {
} finally {
loading.value = false;
}
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
if (req.editCompose && req.dockerCompose == '') {
MsgError(i18n.global.t('app.composeNullErr'));
return;
}
if (req.cpuQuota < 0) {
req.cpuQuota = 0;
}
if (req.memoryLimit < 0) {
req.memoryLimit = 0;
}
if (!isHostMode.value && !req.allowPort) {
ElMessageBox.confirm(i18n.global.t('app.installWarn'), i18n.global.t('app.checkTitle'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
install();
});
} else {
install();
}
});
};
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const install = () => {
loading.value = true;
const taskID = newUUID();
req.taskID = taskID;
installApp(req)
.then(() => {
handleClose();
openTaskLog(taskID);
})
.finally(() => {
loading.value = false;
});
};
const loadLimit = async () => {
const res = await loadResourceLimit();
limits.value = res.data;
limits.value.memory = Number((limits.value.memory / 1024 / 1024).toFixed(2));
oldMemory.value = limits.value.memory;
const acceptParams = async (props: { app: any; params?: any }) => {
open.value = true;
await nextTick();
installFormRef.value?.resetForm();
installFormRef.value?.initForm(props.app.key);
};
defineExpose({
acceptParams,
});
onMounted(() => {
loadLimit();
});
</script>

View file

@ -91,6 +91,14 @@
<el-input v-model="paramModel.specifyIP"></el-input>
<span class="input-help">{{ $t('app.specifyIPHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('container.restartPolicy')" prop="restartPolicy">
<el-select v-model="paramModel.restartPolicy" class="p-w-300">
<el-option :label="$t('container.no')" value="no"></el-option>
<el-option :label="$t('container.always')" value="always"></el-option>
<el-option :label="$t('container.onFailure')" value="on-failure"></el-option>
<el-option :label="$t('container.unlessStopped')" value="unless-stopped"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('container.cpuQuota')" prop="cpuQuota">
<el-input type="number" class="!w-2/5" v-model.number="paramModel.cpuQuota" maxlength="5">
<template #append>{{ $t('app.cpuCore') }}</template>
@ -159,7 +167,7 @@ const loading = ref(false);
const params = ref<EditForm[]>();
const edit = ref(false);
const paramForm = ref<FormInstance>();
const paramModel = ref<any>({
const paramModel = reactive<any>({
params: {},
});
const rules = reactive({
@ -167,8 +175,9 @@ const rules = reactive({
cpuQuota: [Rules.requiredInput, checkNumberRange(0, 999)],
memoryLimit: [Rules.requiredInput, checkNumberRange(0, 9999999999)],
containerName: [Rules.containerName],
restartPolicy: [Rules.requiredSelect],
});
const submitModel = ref<any>({
const submitModel = reactive<any>({
webUI: '',
});
const appType = ref('');
@ -208,10 +217,10 @@ function checkPort(port: string) {
}
const acceptParams = async (props: ParamProps) => {
submitModel.value.installId = props.id;
submitModel.installId = props.id;
params.value = [];
paramData.value.id = props.id;
paramModel.value.params = {};
paramModel.params = {};
edit.value = false;
await get();
open.value = true;
@ -224,14 +233,14 @@ const handleClose = () => {
};
const editParam = () => {
params.value.forEach((param: EditForm) => {
paramModel.value.params[param.key] = param.value;
paramModel.params[param.key] = param.value;
});
edit.value = !edit.value;
};
const changeAllowPort = () => {
if (paramModel.value.allowPort) {
paramModel.value.specifyIP = '';
if (paramModel.allowPort) {
paramModel.specifyIP = '';
}
};
@ -271,15 +280,16 @@ const get = async () => {
}
});
}
paramModel.value.memoryLimit = res.data.memoryLimit;
paramModel.value.cpuQuota = res.data.cpuQuota;
paramModel.value.memoryUnit = res.data.memoryUnit !== '' ? res.data.memoryUnit : 'MB';
paramModel.value.allowPort = res.data.allowPort;
paramModel.value.containerName = res.data.containerName;
paramModel.value.advanced = false;
paramModel.value.dockerCompose = res.data.dockerCompose;
paramModel.value.isHostMode = res.data.hostMode;
paramModel.value.specifyIP = res.data.specifyIP;
paramModel.memoryLimit = res.data.memoryLimit;
paramModel.cpuQuota = res.data.cpuQuota;
paramModel.memoryUnit = res.data.memoryUnit !== '' ? res.data.memoryUnit : 'MB';
paramModel.allowPort = res.data.allowPort;
paramModel.containerName = res.data.containerName;
paramModel.advanced = false;
paramModel.dockerCompose = res.data.dockerCompose;
paramModel.isHostMode = res.data.hostMode;
paramModel.specifyIP = res.data.specifyIP;
paramModel.restartPolicy = res.data.restartPolicy || 'no';
appConfigUpdate.value.webUI = res.data.webUI;
if (res.data.webUI != '') {
const httpConfig = splitHttp(res.data.webUI);
@ -304,22 +314,23 @@ const submit = async (formEl: FormInstance) => {
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
submitModel.value.params = paramModel.value.params;
if (paramModel.value.advanced) {
submitModel.value.advanced = paramModel.value.advanced;
submitModel.value.memoryLimit = paramModel.value.memoryLimit;
submitModel.value.cpuQuota = paramModel.value.cpuQuota;
submitModel.value.memoryUnit = paramModel.value.memoryUnit;
submitModel.value.allowPort = paramModel.value.allowPort;
submitModel.value.containerName = paramModel.value.containerName;
if (paramModel.value.editCompose) {
submitModel.value.editCompose = paramModel.value.editCompose;
submitModel.value.dockerCompose = paramModel.value.dockerCompose;
submitModel.params = paramModel.params;
if (paramModel.advanced) {
submitModel.advanced = paramModel.advanced;
submitModel.memoryLimit = paramModel.memoryLimit;
submitModel.cpuQuota = paramModel.cpuQuota;
submitModel.memoryUnit = paramModel.memoryUnit;
submitModel.allowPort = paramModel.allowPort;
submitModel.containerName = paramModel.containerName;
if (paramModel.editCompose) {
submitModel.editCompose = paramModel.editCompose;
submitModel.dockerCompose = paramModel.dockerCompose;
}
submitModel.restartPolicy = paramModel.restartPolicy;
}
try {
loading.value = true;
await updateAppInstallParams(submitModel.value);
await updateAppInstallParams(submitModel);
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
handleClose();

View file

@ -51,7 +51,6 @@ const getApps = async () => {
try {
const res = await getIgnoredApp();
apps.value = res.data;
console.log(apps.value);
} catch (error) {}
};

View file

@ -45,7 +45,6 @@ let open = ref(false);
const acceptParams = (props: InstallProps) => {
installData.value = props.items;
console.log('acceptParams', props.items);
open.value = true;
};

View file

@ -100,48 +100,21 @@
</el-form-item>
<div v-if="website.appType == 'new'">
<el-form-item :label="$t('app.app')" prop="appinstall.appId">
<el-row :gutter="20">
<el-col :span="12">
<el-select
v-model="website.appinstall.appId"
@change="changeApp()"
class="p-w-200"
filterable
>
<el-option
v-for="(app, index) in apps"
:key="index"
:label="app.name"
:value="app.id"
></el-option>
</el-select>
</el-col>
<el-col :span="12">
<el-select
v-model="website.appinstall.version"
@change="getDetail(website.appinstall.version)"
class="p-w-200"
>
<el-option
v-for="(version, index) in appVersions"
:key="index"
:label="version"
:value="version"
></el-option>
</el-select>
</el-col>
</el-row>
<el-select
v-model="website.appinstall.appId"
@change="changeApp()"
class="p-w-300"
filterable
>
<el-option
v-for="(app, index) in apps"
:key="index"
:label="app.name"
:value="app.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('commons.table.name')" prop="appinstall.name">
<el-input v-model.trim="website.appinstall.name"></el-input>
</el-form-item>
<Params
:key="paramKey"
v-model:form="website.appinstall.params"
v-model:rules="rules.appinstall.params"
:params="appParams"
:propStart="'appinstall.params.'"
></Params>
<AppInstallForm ref="installFormRef" v-model="website.appinstall" :loading="loading" />
</div>
</div>
<div v-if="website.type === 'subsite'">
@ -246,49 +219,6 @@
{{ $t('website.runtimePortWarn') }}
</el-text>
</div>
<el-form-item prop="advanced" v-if="website.type === 'deployment' && website.appType === 'new'">
<el-checkbox v-model="website.appinstall.advanced" :label="$t('app.advanced')" size="large" />
</el-form-item>
<div v-if="website.appinstall.advanced">
<el-form-item :label="$t('app.containerName')" prop="containerName">
<el-input
v-model.trim="website.appinstall.containerName"
:placeholder="$t('app.containerNameHelper')"
></el-input>
</el-form-item>
<el-form-item :label="$t('container.cpuQuota')" prop="appinstall.cpuQuota">
<el-input
type="number"
style="width: 40%"
v-model.number="website.appinstall.cpuQuota"
maxlength="5"
>
<template #append>{{ $t('app.cpuCore') }}</template>
</el-input>
<span class="input-help">{{ $t('container.limitHelper', [99999]) }}</span>
</el-form-item>
<el-form-item :label="$t('container.memoryLimit')" prop="appinstall.memoryLimit">
<el-input style="width: 40%" v-model.number="website.appinstall.memoryLimit" maxlength="10">
<template #append>
<el-select
v-model="website.appinstall.memoryUnit"
placeholder="Select"
class="pre-select"
>
<el-option label="KB" value="K" />
<el-option label="MB" value="M" />
<el-option label="GB" value="G" />
</el-select>
</template>
</el-input>
<span class="input-help">{{ $t('container.limitHelper', ['9999999999']) }}</span>
</el-form-item>
<el-form-item prop="allowPort" v-if="website.type === 'deployment'">
<el-checkbox v-model="website.appinstall.allowPort" :label="$t('app.allowPort')" size="large" />
<span class="input-help">{{ $t('app.allowPortHelper') }}</span>
</el-form-item>
</div>
<DomainCreate v-model:form="website" @gengerate="websiteForm.clearValidate()"></DomainCreate>
<el-form-item prop="IPV6">
<el-checkbox v-model="website.IPV6" :label="$t('website.ipv6')" size="large" />
@ -521,7 +451,7 @@
<script lang="ts" setup name="CreateWebSite">
import { App } from '@/api/interface/app';
import { getAppByKey, getAppDetail, searchApp, getAppInstalled } from '@/api/modules/app';
import { searchApp, getAppInstalled } from '@/api/modules/app';
import {
createWebsite,
getWebsiteOptions,
@ -534,7 +464,6 @@ import { Rules, checkNumberRange } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm, FormInstance } from 'element-plus';
import { reactive, ref } from 'vue';
import Params from '@/views/app-store/detail/params/index.vue';
import Check from '../check/index.vue';
import { MsgError, MsgSuccess } from '@/utils/message';
import { getAgentGroupList } from '@/api/modules/group';
@ -550,6 +479,7 @@ import { Website } from '@/api/interface/website';
import DomainCreate from '@/views/website/website/domain-create/index.vue';
import { getPathByType } from '@/api/modules/files';
import { getWebsiteTypes } from '@/global/mimetype';
import AppInstallForm from '@/views/app-store/detail/form/index.vue';
const websiteForm = ref<FormInstance>();
@ -646,10 +576,6 @@ const appReq = reactive({
pageSize: 100,
});
const apps = ref<App.App[]>([]);
const appVersions = ref<string[]>([]);
const appDetail = ref<App.AppDetail>();
const appParams = ref<App.AppParams>();
const paramKey = ref(1);
const preCheckRef = ref();
const staticPath = ref('');
const runtimeResource = ref('appstore');
@ -671,6 +597,7 @@ const parentWebsites = ref();
const dirs = ref([]);
const runtimePorts = ref([]);
const WebsiteTypes = getWebsiteTypes();
const installFormRef = ref();
const handleClose = () => {
open.value = false;
@ -747,7 +674,7 @@ const searchAppList = () => {
if (res.data.items.length > 0) {
website.value.appinstall.appId = res.data.items[0].id;
website.value.appinstall.appkey = res.data.items[0].key;
getApp();
changeApp();
}
});
};
@ -756,30 +683,11 @@ const changeApp = () => {
apps.value.forEach((app) => {
if (app.id === website.value.appinstall.appId) {
website.value.appinstall.appkey = app.key;
getApp();
installFormRef.value.initForm(app.key);
}
});
};
const getApp = () => {
getAppByKey(website.value.appinstall.appkey).then((res) => {
appVersions.value = res.data.versions;
if (res.data.versions.length > 0) {
website.value.appinstall.version = res.data.versions[0];
getDetail(res.data.versions[0]);
}
});
};
const getDetail = (version: string) => {
getAppDetail(website.value.appinstall.appId, version, 'app').then((res) => {
website.value.appinstall.appDetailId = res.data.id;
appDetail.value = res.data;
appParams.value = res.data.params;
paramKey.value++;
});
};
const changeRuntimeType = () => {
runtimeReq.value.type = website.value.runtimeType;
website.value.appinstall.advanced = false;
@ -893,6 +801,10 @@ const submit = async (formEl: FormInstance | undefined) => {
if (!valid) {
return;
}
if (website.value.type == 'deployment' && website.value.appType === 'new') {
const isValid = await installFormRef.value?.validate();
if (!isValid) return;
}
if (website.value.type === 'runtime' && website.value.runtimeType !== 'php' && website.value.port == 0) {
MsgError(i18n.global.t('website.runtimePortWarn'));
return;