feat: Container compose supports .env file operations (#11274)

This commit is contained in:
ssongliu
2025-12-09 09:36:58 +00:00
committed by GitHub
parent b47a4525dc
commit 9aa3caab16
14 changed files with 341 additions and 295 deletions
+3 -3
View File
@@ -404,7 +404,7 @@ func recreateCompose(content, path string) error {
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
for i := 0; i < len(list); i++ {
envFilePath := path.Join(path.Dir(list[i].Path), "1panel.env")
envFilePath := path.Join(path.Dir(list[i].Path), ".env")
file, err := os.ReadFile(envFilePath)
if err != nil {
continue
@@ -424,7 +424,7 @@ func newComposeEnv(pathItem string, env []string) error {
if len(env) == 0 {
return nil
}
envFilePath := path.Join(path.Dir(pathItem), "1panel.env")
envFilePath := path.Join(path.Dir(pathItem), ".env")
file, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, constant.FilePerm)
if err != nil {
global.LOG.Errorf("failed to create env file: %v", err)
@@ -438,6 +438,6 @@ func newComposeEnv(pathItem string, env []string) error {
return err
}
}
global.LOG.Infof("1panel.env file successfully created or updated with env variables in %s", envFilePath)
global.LOG.Infof(".env file successfully created or updated with env variables in %s", envFilePath)
return nil
}
+79
View File
@@ -0,0 +1,79 @@
<template>
<div>
<el-space wrap>
<div v-for="(env, index) in tmpLabels" :key="index">
<fu-read-write-switch write-trigger="onDblclick">
<template #read>
<el-button plain>
{{ env }}
<el-button
circle
plain
size="small"
type="danger"
class="float-right -mr-6 -mt-8"
icon="Close"
@click="remove(index)"
/>
</el-button>
</template>
<template #default="{ read }">
<el-input class="p-w-300" v-model="tmpLabels[index]" @blur="read" />
</template>
</fu-read-write-switch>
</div>
<el-input v-if="showAdd" v-model="labelItem">
<template #append>
<el-button icon="Check" @click="save()" />
<el-button icon="Close" @click="cancel()" />
</template>
</el-input>
<el-button plain icon="Plus" @click="add()" />
</el-space>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
const em = defineEmits(['update:labels']);
const tmpLabels = ref([]);
const labelItem = ref('');
const showAdd = ref(false);
const props = defineProps({
labels: { type: Array<string>, default: [] },
});
watch(
() => props.labels,
(newVal) => {
tmpLabels.value = newVal || [];
},
);
const add = () => {
showAdd.value = true;
labelItem.value = '';
};
const cancel = () => {
showAdd.value = false;
labelItem.value = '';
};
const save = () => {
if (labelItem.value && tmpLabels.value.indexOf(labelItem.value) === -1) {
tmpLabels.value.push(labelItem.value);
}
showAdd.value = false;
labelItem.value = '';
handleUpdate();
};
const remove = (index: number) => {
tmpLabels.value.splice(index, 1);
handleUpdate();
};
const handleUpdate = () => {
em('update:labels', tmpLabels.value);
};
onMounted(() => {
tmpLabels.value = props.labels || [];
});
</script>
+3 -2
View File
@@ -852,8 +852,6 @@ const message = {
privileged: 'Privileged',
privilegedHelper:
'Allow the container to perform certain privileged operations on the host, which may increase container risks. Use with caution!',
editComposeHelper:
'Note: Environment variables are saved to the 1panel.env file and need to be referenced via env_file in compose.\nVariables only take effect inside the container and do not participate in ${VAR} substitution in the compose file.',
upgradeHelper: 'Repository Name/Image Name: Image Version',
upgradeWarning2:
@@ -1008,6 +1006,9 @@ const message = {
composeOperatorHelper: '{1} operation will be performed on {0}. Do you want to continue?',
composeDownHelper:
'This will stop and remove all containers and networks under the {0} compose. Do you want to continue?',
composeEnvHelper1: 'Double-click to edit existing environment variables.',
composeEnvHelper2:
'This orchestration was created by the 1Panel App Store. Please modify environment variables in the installed applications.',
setting: 'Setting | Settings',
goSetting: 'Go to edit',
+4 -2
View File
@@ -856,8 +856,6 @@ const message = {
privileged: 'Privilegiado',
privilegedHelper:
'Permite que el contenedor realice ciertas operaciones privilegiadas en el host, lo que puede aumentar los riesgos. ¡Úselo con precaución!',
editComposeHelper:
'Nota: Las variables de entorno se guardan en el archivo 1panel.env y deben ser referenciadas mediante env_file en compose.\nLas variables solo tienen efecto dentro del contenedor y no participan en la sustitución ${VAR} en el archivo compose.',
upgradeHelper: 'Nombre de repositorio/imagen: versión de la imagen',
upgradeWarning2:
'La operación de actualización requiere reconstruir el contenedor, cualquier dato no persistente se perderá. ¿Desea continuar?',
@@ -1012,6 +1010,10 @@ const message = {
composeOperatorHelper: 'La operación {1} se realizará en {0}. ¿Desea continuar?',
composeDownHelper:
'Esto detendrá y eliminará todos los contenedores y redes bajo la composición {0}. ¿Desea continuar?',
composeEnvHelper1: 'Haga doble clic para editar las variables de entorno existentes.',
composeEnvHelper2:
'Esta orquestación fue creada por la Tienda de Aplicaciones 1Panel. Modifique las variables de entorno en las aplicaciones instaladas.',
setting: 'Configuración | Configuraciones',
goSetting: 'Ir a editar',
operatorStatusHelper: 'Esto "{0}" el servicio Docker. ¿Desea continuar?',
+3
View File
@@ -984,6 +984,9 @@ const message = {
composeOperatorHelper: '{1}操作は{0}で実行されます続けたいですか',
composeDownHelper:
'これにより{0}構成の下のすべてのコンテナとネットワークが停止して削除されます続けたいですか',
composeEnvHelper1: '既存の環境変数を編集するにはダブルクリックしてください',
composeEnvHelper2:
'このオーケストレーションは1Panelアプリストアで作成されましたインストール済みアプリケーションで環境変数を変更してください',
setting: '設定|設定',
operatorStatusHelper: 'これは{0}Dockerサービスになります続けたいですか',
+3 -2
View File
@@ -827,8 +827,6 @@ const message = {
privileged: '특권 모드',
privilegedHelper:
'컨테이너가 호스트에서 특정 특권 작업을 수행할 수 있도록 허용합니다. 이는 보안 위험을 초래할 수 있으므로 주의해서 사용하십시오.',
editComposeHelper:
'참고: 환경 변수는 1panel.env 파일에 저장되며 compose에서 env_file을 통해 참조해야 합니다.\n변수는 컨테이너 내부에서만 유효하며 compose 파일의 ${VAR} 치환에 참여하지 않습니다.',
upgradeHelper: '레포지토리 이름/이미지 이름: 이미지 버전',
upgradeWarning2:
'업그레이드 작업은 컨테이너를 재빌드해야 하며, 비지속적인 데이터가 손실됩니다. 계속하시겠습니까?',
@@ -974,6 +972,9 @@ const message = {
composeOperatorHelper: '{1} 작업이 {0}에서 수행됩니다. 계속 하시겠습니까?',
composeDownHelper:
'이 작업은 {0} 컴포즈 아래의 모든 컨테이너와 네트워크를 중지하고 제거합니다. 계속 하시겠습니까?',
composeEnvHelper1: '기존 환경 변수를 편집하려면 더블 클릭하세요.',
composeEnvHelper2:
'이 오케스트레이션은 1Panel 앱 스토어에서 생성되었습니다. 설치된 애플리케이션에서 환경 변수를 수정하세요.',
setting: '설정 | 설정들',
operatorStatusHelper: '이 작업은 Docker 서비스를 "{0}" 합니다. 계속 하시겠습니까?',
+3 -2
View File
@@ -846,8 +846,6 @@ const message = {
privileged: 'Privileged',
privilegedHelper:
'Benarkan kontena menjalankan operasi teristimewa tertentu pada hos, yang boleh meningkatkan risiko kontena. Gunakan dengan berhati-hati!',
editComposeHelper:
'Nota: Pembolehubah persekitaran disimpan ke fail 1panel.env dan perlu dirujuk melalui env_file dalam compose.\nPembolehubah hanya berkesan di dalam bekas dan tidak mengambil bahagian dalam penggantian ${VAR} dalam fail compose.',
upgradeHelper: 'Nama Repository/Nama Imej: Versi Imej',
upgradeWarning2:
@@ -1002,6 +1000,9 @@ const message = {
composeOperatorHelper: 'Operasi {1} akan dilakukan pada {0}. Adakah anda mahu meneruskan?',
composeDownHelper:
'Ini akan menghentikan dan menghapuskan semua kontena dan rangkaian di bawah komposisi {0}. Adakah anda mahu meneruskan?',
composeEnvHelper1: 'Klik dua kali untuk mengedit pembolehubah persekitaran sedia ada.',
composeEnvHelper2:
'Penyelarasan ini dibuat oleh Kedai Apl 1Panel. Sila ubah pembolehubah persekitaran dalam aplikasi yang dipasang.',
setting: 'Tetapan | Tetapan',
operatorStatusHelper: 'Ini akan "{0}" perkhidmatan Docker. Adakah anda mahu meneruskan?',
+3 -2
View File
@@ -844,8 +844,6 @@ const message = {
privileged: 'Privilegiado',
privilegedHelper:
'Permite que o contêiner execute determinadas operações privilegiadas no host, o que pode aumentar os riscos do contêiner. Use com cautela!',
editComposeHelper:
'Nota: As variáveis de ambiente são salvas no arquivo 1panel.env e precisam ser referenciadas via env_file no compose.\nAs variáveis só têm efeito dentro do contêiner e não participam da substituição ${VAR} no arquivo compose.',
upgradeHelper: 'Nome do Repositório/Nome da Imagem: Versão da Imagem',
upgradeWarning2:
'A operação de upgrade requer a reconstrução do contêiner, e qualquer dado não persistente será perdido. Deseja continuar?',
@@ -999,6 +997,9 @@ const message = {
composeOperatorHelper: 'A operação {1} será realizada no {0}. Deseja continuar?',
composeDownHelper:
'Isso irá parar e remover todos os containers e redes sob a composição {0}. Deseja continuar?',
composeEnvHelper1: 'Clique duas vezes para editar variáveis de ambiente existentes.',
composeEnvHelper2:
'Esta orquestração foi criada pela Loja de Aplicativos 1Panel. Modifique as variáveis de ambiente nos aplicativos instalados.',
setting: 'Configuração | Configurações',
operatorStatusHelper: 'Isso irá "{0}" o serviço Docker. Deseja continuar?',
+3 -2
View File
@@ -840,8 +840,6 @@ const message = {
privileged: 'Привилегированный',
privilegedHelper:
'Разрешить контейнеру выполнять определенные привилегированные операции на хосте, что может повысить риски контейнера. Используйте с осторожностью!',
editComposeHelper:
'Примечание: Переменные среды сохраняются в файл 1panel.env и должны быть указаны через env_file в compose.\nПеременные действуют только внутри контейнера и не участвуют в замене ${VAR} в файле compose.',
upgradeHelper: 'Имя репозитория/Имя образа: Версия образа',
upgradeWarning2:
@@ -996,6 +994,9 @@ const message = {
composeDetailHelper: 'Compose создан вне 1Panel. Операции запуска и остановки не поддерживаются.',
composeOperatorHelper: 'Операция {1} будет выполнена для {0}. Хотите продолжить?',
composeDownHelper: 'Это остановит и удалит все контейнеры и сети под compose {0}. Хотите продолжить?',
composeEnvHelper1: 'Дважды щелкните, чтобы редактировать существующие переменные среды.',
composeEnvHelper2:
'Эта оркестрация создана Магазином приложений 1Panel. Измените переменные среды в установленных приложениях.',
setting: 'Настройка | Настройки',
operatorStatusHelper: 'Это выполнит "{0}" службы Docker. Хотите продолжить?',
+3 -2
View File
@@ -860,8 +860,6 @@ const message = {
privileged: 'Ayrıcalıklı',
privilegedHelper:
'Konteynerin ana bilgisayarda belirli ayrıcalıklı işlemler gerçekleştirmesine izin verir, bu da konteyner risklerini artırabilir. Dikkatli kullanın!',
editComposeHelper:
'Not: Ortam değişkenleri 1panel.env dosyasına kaydedilir ve compose içinde env_file aracılığıyla referans alınmalıdır.\nDeğişkenler yalnızca konteyner içinde geçerlidir ve compose dosyasındaki ${VAR} değiştirmesine katılmaz.',
upgradeHelper: 'Depo Adı/İmaj Adı: İmaj Sürümü',
upgradeWarning2:
@@ -1019,6 +1017,9 @@ const message = {
composeOperatorHelper: '{0} üzerinde {1} işlemi gerçekleştirilecek. Devam etmek istiyor musunuz?',
composeDownHelper:
'Bu, {0} compose altındaki tüm konteynerleri ve ağları durduracak ve kaldıracaktır. Devam etmek istiyor musunuz?',
composeEnvHelper1: 'Mevcut ortam değişkenlerini düzenlemek için çift tıklayın.',
composeEnvHelper2:
'Bu düzenleme 1Panel Uygulama Mağazası tarafından oluşturuldu. Lütfen ortam değişkenlerini yüklü uygulamalarda değiştirin.',
setting: 'Ayar | Ayarlar',
goSetting: 'Düzenlemeye git',
+2 -2
View File
@@ -813,8 +813,6 @@ const message = {
emptyUser: '為空時將使用容器預設的使用者登入',
privileged: '特權模式',
privilegedHelper: '允許容器在主機上執行某些特權操作可能會增加容器風險請謹慎開啟',
editComposeHelper:
'注意環境變數儲存至 1panel.env 檔案需在 compose 中透過 env_file 引用\n變數僅在容器內部生效不參與 compose 檔案的 ${VAR} 替換',
upgradeHelper: '倉庫名稱/鏡像名稱:鏡像版本',
upgradeWarning2: '升級操作需要重建容器任何未持久化的資料將會遺失是否繼續',
@@ -959,6 +957,8 @@ const message = {
composeDetailHelper: ' compose 1Panel 編排外部建立暫不支援啟停操作',
composeOperatorHelper: '將對 {0} 進行 {1} 操作是否繼續',
composeDownHelper: '將停止並刪除 {0} 編排下所有容器及網路是否繼續',
composeEnvHelper1: '雙擊以編輯現有環境變數',
composeEnvHelper2: '該編排為 1Panel 應用商店建立請在已安裝應用中修改環境變數',
setting: '配置',
goSetting: '去修改',
+2 -2
View File
@@ -814,8 +814,6 @@ const message = {
emptyUser: '为空时将使用容器默认的用户登录',
privileged: '特权模式',
privilegedHelper: '允许容器在主机上执行某些特权操作可能会增加容器风险谨慎开启',
editComposeHelper:
'注意环境变量保存至 1panel.env 文件需在 compose 中通过 env_file 引用\n变量仅在容器内部生效不参与 compose 文件的 ${VAR} 替换',
upgradeHelper: '仓库名称/镜像名称:镜像版本',
upgradeWarning2: '升级操作需要重建容器任何未持久化的数据将会丢失是否继续',
@@ -961,6 +959,8 @@ const message = {
composeDetailHelper: ' compose 1Panel 编排外部创建暂不支持启停操作',
composeOperatorHelper: '将对 {0} 进行 {1} 操作是否继续',
composeDownHelper: '将停止并删除 {0} 编排下所有容器及网络是否继续',
composeEnvHelper1: '双击以编辑已有环境变量',
composeEnvHelper2: '该编排为 1Panel 应用商店创建请在已安装应用中修改环境变量',
setting: '配置',
goSetting: '去修改',
@@ -1,241 +0,0 @@
<template>
<DrawerPro
v-model="drawerVisible"
:header="$t('container.compose')"
@close="handleClose"
size="large"
:autoClose="false"
:fullScreen="true"
>
<el-form ref="formRef" @submit.prevent label-position="top" :model="form" :rules="rules" v-loading="loading">
<el-form-item :label="$t('app.source')">
<el-radio-group v-model="form.from" @change="onEdit('form')">
<el-radio value="edit">{{ $t('commons.button.edit') }}</el-radio>
<el-radio value="path">{{ $t('container.pathSelect') }}</el-radio>
<el-radio value="template">{{ $t('container.composeTemplate') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.from === 'path'" prop="path">
<el-input
@change="onEdit('')"
:placeholder="$t('commons.example') + '/tmp/docker-compose.yml'"
v-model="form.path"
>
<template #prepend>
<el-button icon="Folder" @click="fileRef.acceptParams({ dir: false })" />
</template>
</el-input>
</el-form-item>
<el-form-item v-if="form.from === 'template'" prop="template">
<el-select v-model="form.template" @change="onEdit('template')">
<template #prefix>{{ $t('container.template') }}</template>
<el-option v-for="item in templateOptions" :key="item.id" :value="item.id" :label="item.name" />
</el-select>
</el-form-item>
<el-form-item v-if="form.from === 'edit' || form.from === 'template'" prop="name">
<el-input @input="changePath" @change="onEdit('')" v-model.trim="form.name">
<template #prefix>
<span style="margin-right: 8px">{{ $t('file.dir') }}</span>
</template>
</el-input>
<span class="input-help">
{{ $t('container.composePathHelper', [composeFile]) }}
</span>
</el-form-item>
<el-form-item>
<div v-if="form.from === 'edit' || form.from === 'template'" class="w-full">
<CodemirrorPro
v-model="form.file"
placeholder="#Define or paste the content of your docker-compose file here"
mode="yaml"
:heightDiff="400"
></CodemirrorPro>
</div>
</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>
<span class="input-help">{{ $t('container.editComposeHelper') }}</span>
<CodemirrorPro v-model="form.envFileContent" :height="45" :minHeight="45" disabled mode="yaml" />
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</DrawerPro>
<TaskLog ref="taskLogRef" width="70%">
<template #task-footer>
<el-button @click="handleClose">{{ $t('commons.table.backToList') }}</el-button>
<el-button type="primary" @click="closeTask">{{ $t('commons.table.keepEdit') }}</el-button>
</template>
</TaskLog>
<FileList ref="fileRef" @choose="loadDir" />
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import FileList from '@/components/file-list/index.vue';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm, ElMessageBox } from 'element-plus';
import { loadBaseDir } from '@/api/modules/setting';
import { MsgError, MsgSuccess } from '@/utils/message';
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import TaskLog from '@/components/log/task/index.vue';
import { listComposeTemplate, testCompose, upCompose } from '@/api/modules/container';
import { newUUID } from '@/utils/util';
const loading = ref();
const oldFrom = ref('edit');
const drawerVisible = ref(false);
const templateOptions = ref();
const baseDir = ref();
const composeFile = ref();
const taskLogRef = ref();
const fileRef = ref();
const form = reactive({
taskID: '',
name: '',
from: 'edit',
path: '',
file: '',
template: null as number,
env: [],
envStr: '',
envFileContent: `env_file:\n - 1panel.env`,
});
const rules = reactive({
name: [Rules.requiredInput, Rules.composeName],
path: [Rules.requiredInput],
template: [Rules.requiredSelect],
});
const loadTemplates = async () => {
const res = await listComposeTemplate();
templateOptions.value = res.data;
};
const acceptParams = (): void => {
drawerVisible.value = true;
form.name = '';
form.from = 'edit';
form.path = '';
form.file = '';
form.template = null;
form.env = [];
form.envStr = '';
loadTemplates();
loadPath();
};
const emit = defineEmits<{ (e: 'search'): void }>();
const changeTemplate = () => {
for (const item of templateOptions.value) {
if (form.template === item.id) {
form.file = item.content;
break;
}
}
};
const changeFrom = () => {
if ((oldFrom.value === 'edit' || oldFrom.value === 'template') && form.file) {
ElMessageBox.confirm(i18n.global.t('container.fromChangeHelper'), i18n.global.t('app.source'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
})
.then(() => {
if (oldFrom.value === 'template') {
form.template = null;
form.file = '';
}
if (oldFrom.value === 'edit') {
form.file = '';
}
oldFrom.value = form.from;
})
.catch(() => {
form.from = oldFrom.value;
});
} else {
oldFrom.value = form.from;
}
};
const handleClose = () => {
emit('search');
taskLogRef.value?.handleClose();
drawerVisible.value = false;
};
const closeTask = () => {
taskLogRef.value?.handleClose();
};
const loadPath = async () => {
const pathRes = await loadBaseDir();
baseDir.value = pathRes.data;
changePath();
};
const changePath = async () => {
composeFile.value = baseDir.value + '/docker/compose/' + form.name;
};
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const onEdit = (item: string) => {
if (item === 'template') {
changeTemplate();
}
if (item === 'form') {
changeFrom();
}
};
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
if ((form.from === 'edit' || form.from === 'template') && form.file.length === 0) {
MsgError(i18n.global.t('container.contentEmpty'));
return;
}
if (form.envStr) {
form.env = form.envStr.split('\n');
}
loading.value = true;
await testCompose(form)
.then(async (res) => {
loading.value = false;
if (res.data) {
form.taskID = newUUID();
await upCompose(form);
openTaskLog(form.taskID);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
}
})
.catch(() => {
loading.value = false;
});
});
};
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const loadDir = async (path: string) => {
form.path = path;
};
defineExpose({
acceptParams,
});
</script>
+230 -33
View File
@@ -107,7 +107,7 @@
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="16" :lg="16" :xl="18">
<el-card v-if="currentCompose" v-loading="detailLoading">
<el-card v-if="currentCompose && !isOnCreate" v-loading="detailLoading">
<el-table
v-if="composeContainers.length > 0"
:data="tableData"
@@ -225,37 +225,31 @@
<el-radio-button value="compose">{{ $t('container.compose') }}</el-radio-button>
<el-radio-button value="log">{{ $t('commons.button.log') }}</el-radio-button>
</el-radio-group>
<el-form ref="formRef" v-show="showType === 'compose'" @submit.prevent label-position="top">
<div v-show="showType === 'compose'">
<CodemirrorPro
v-model="composeContent"
mode="yaml"
:heightDiff="475"
placeholder="#Define or paste the content of your docker-compose file here"
/>
<div v-if="currentCompose.createdBy === '1Panel'">
<el-form-item :label="$t('container.env')" prop="environmentStr">
<el-input
type="textarea"
:placeholder="$t('container.tagHelper')"
:rows="3"
v-model="envStr"
/>
</el-form-item>
<span class="input-help whitespace-break-spaces">
{{ $t('container.editComposeHelper') }}
</span>
<CodemirrorPro
v-model="env"
:height="45"
:minHeight="45"
disabled
mode="yaml"
></CodemirrorPro>
</div>
<el-button type="primary" class="float-right mt-2" @click="onSubmitEdit">
<span class="envTitle">{{ $t('container.env') }}</span>
<Label v-if="currentCompose.createdBy !== 'Apps'" v-model:labels="envs" />
<el-space wrap v-else>
<el-button v-for="(env, index) in envs" :key="index" plain>
{{ env }}
</el-button>
</el-space>
<span v-if="currentCompose.createdBy !== 'Apps'" class="input-help">
{{ $t('container.composeEnvHelper1') }}
</span>
<span v-if="currentCompose.createdBy === 'Apps'" class="input-help">
{{ $t('container.composeEnvHelper2') }}
</span>
<el-button type="primary" class="mt-2" @click="onSubmitEdit">
{{ $t('commons.button.save') }}
</el-button>
</el-form>
</div>
<div v-show="showType === 'log'">
<ContainerLog
@@ -268,13 +262,83 @@
/>
</div>
</el-card>
<el-card v-else>
<el-form
ref="formRef"
@submit.prevent
label-position="top"
:model="form"
:rules="rules"
v-loading="loading"
>
<el-form-item :label="$t('app.source')">
<el-radio-group v-model="form.from" @change="onEdit('form')">
<el-radio value="edit">{{ $t('commons.button.edit') }}</el-radio>
<el-radio value="path">{{ $t('container.pathSelect') }}</el-radio>
<el-radio value="template">{{ $t('container.composeTemplate') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.from === 'path'" prop="path">
<el-input
@change="onEdit('')"
:placeholder="$t('commons.example') + '/tmp/docker-compose.yml'"
v-model="form.path"
>
<template #prepend>
<el-button icon="Folder" @click="fileRef.acceptParams({ dir: false })" />
</template>
</el-input>
</el-form-item>
<el-form-item v-if="form.from === 'template'" prop="template">
<el-select v-model="form.template" @change="onEdit('template')">
<template #prefix>{{ $t('container.template') }}</template>
<el-option
v-for="item in templateOptions"
:key="item.id"
:value="item.id"
:label="item.name"
/>
</el-select>
</el-form-item>
<el-form-item v-if="form.from === 'edit' || form.from === 'template'" prop="name">
<el-input @input="changePath" @change="onEdit('')" v-model.trim="form.name">
<template #prefix>
<span style="margin-right: 8px">{{ $t('file.dir') }}</span>
</template>
</el-input>
<span class="input-help">
{{ $t('container.composePathHelper', [composeFile]) }}
</span>
</el-form-item>
<el-form-item>
<div v-if="form.from === 'edit' || form.from === 'template'" class="w-full">
<CodemirrorPro
v-model="form.file"
placeholder="#Define or paste the content of your docker-compose file here"
mode="yaml"
:heightDiff="400"
></CodemirrorPro>
</div>
</el-form-item>
<span class="envTitle">{{ $t('container.env') }}</span>
<Label v-model:labels="form.env" />
<span v-if="currentCompose.createdBy !== 'Apps'" class="input-help">
{{ $t('container.composeEnvHelper1') }}
</span>
</el-form>
<el-button type="primary" class="mt-2" @click="onSubmit(formRef)">
{{ $t('commons.button.save') }}
</el-button>
</el-card>
</el-col>
</el-row>
<el-empty v-else :description="$t('commons.msg.noneData')" />
</template>
</LayoutContent>
<CreateDialog @search="search" ref="dialogCreateRef" />
<TaskLog ref="taskLogRef" width="70%" @close="search(true)" />
<FileList ref="fileRef" @choose="loadDir" />
<DeleteDialog @search="search(true)" ref="dialogDelRef" />
<ContainerInspectDialog ref="containerInspectRef" />
<TerminalDialog ref="terminalDialogRef" />
@@ -286,10 +350,12 @@
import { computed, ref } from 'vue';
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import ContainerLog from '@/components/log/container/index.vue';
import TaskLog from '@/components/log/task/index.vue';
import FileList from '@/components/file-list/index.vue';
import Label from '@/components/label/index.vue';
import ContainerInspectDialog from '@/views/container/container/inspect/index.vue';
import TerminalDialog from '@/views/container/container/terminal/index.vue';
import ContainerLogDialog from '@/components/log/container-drawer/index.vue';
import CreateDialog from '@/views/container/compose/create/index.vue';
import DeleteDialog from '@/views/container/compose/delete/index.vue';
import {
composeOperator,
@@ -297,14 +363,20 @@ import {
containerItemStats,
containerListStats,
inspect,
listComposeTemplate,
searchCompose,
testCompose,
upCompose,
} from '@/api/modules/container';
import DockerStatus from '@/views/container/docker-status/index.vue';
import i18n from '@/lang';
import { Container } from '@/api/interface/container';
import { routerToFileWithPath } from '@/utils/router';
import { MsgSuccess } from '@/utils/message';
import { computeCPU, computeSize2, computeSizeForDocker } from '@/utils/util';
import { MsgError, MsgSuccess } from '@/utils/message';
import { computeCPU, computeSize2, computeSizeForDocker, newUUID } from '@/utils/util';
import { Rules } from '@/global/form-rules';
import { loadBaseDir } from '@/api/modules/setting';
import { ElForm } from 'element-plus';
const data = ref<any[]>([]);
const loading = ref(false);
@@ -312,10 +384,7 @@ const detailLoading = ref(false);
const currentCompose = ref<Container.ComposeInfo | null>(null);
const composeContainers = ref([]);
const composeContent = ref('');
const envStr = ref('');
const env = ref('env_file:\n - 1panel.env');
const dialogCreateRef = ref();
const dialogDelRef = ref();
const containerInspectRef = ref();
const terminalDialogRef = ref();
@@ -324,6 +393,31 @@ const containerLogDialogRef = ref();
const searchName = ref('');
const showType = ref('compose');
const containerStats = ref<any[]>([]);
const envs = ref([]);
const isOnCreate = ref();
const oldFrom = ref('edit');
const templateOptions = ref();
const baseDir = ref();
const composeFile = ref();
const taskLogRef = ref();
const fileRef = ref();
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const form = reactive({
taskID: '',
name: '',
from: 'edit',
path: '',
file: '',
template: null as number,
env: [],
});
const rules = reactive({
name: [Rules.requiredInput, Rules.composeName],
path: [Rules.requiredInput],
template: [Rules.requiredSelect],
});
const isActive = ref(false);
const isExist = ref(false);
@@ -376,6 +470,7 @@ const search = async (withRefreshDetail?: boolean) => {
page: 1,
pageSize: 100,
};
isOnCreate.value = false;
loading.value = true;
await searchCompose(params)
.then((res) => {
@@ -394,8 +489,10 @@ const loadDetail = async (row: Container.ComposeInfo, withRefresh: boolean) => {
if (currentCompose.value?.name === row.name && withRefresh !== true) {
return;
}
isOnCreate.value = false;
detailLoading.value = true;
currentCompose.value = row;
envs.value = row.env || [];
composeContainers.value = row.containers || [];
await inspect({ id: currentCompose.value.name, type: 'compose' })
.then((res) => {
@@ -418,7 +515,71 @@ const loadContainerStats = async () => {
};
const onOpenDialog = async () => {
dialogCreateRef.value!.acceptParams();
isOnCreate.value = true;
loadTemplates();
form.name = '';
form.from = 'edit';
form.path = '';
form.file = '';
form.template = null;
form.env = [];
loadPath();
loadTemplates();
};
const onEdit = (item: string) => {
if (item === 'template') {
changeTemplate();
}
if (item === 'form') {
changeFrom();
}
};
const changeTemplate = () => {
for (const item of templateOptions.value) {
if (form.template === item.id) {
form.file = item.content;
break;
}
}
};
const changeFrom = () => {
if ((oldFrom.value === 'edit' || oldFrom.value === 'template') && form.file) {
ElMessageBox.confirm(i18n.global.t('container.fromChangeHelper'), i18n.global.t('app.source'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
})
.then(() => {
if (oldFrom.value === 'template') {
form.template = null;
form.file = '';
}
if (oldFrom.value === 'edit') {
form.file = '';
}
oldFrom.value = form.from;
})
.catch(() => {
form.from = oldFrom.value;
});
} else {
oldFrom.value = form.from;
}
};
const loadTemplates = async () => {
const res = await listComposeTemplate();
templateOptions.value = res.data;
};
const loadPath = async () => {
const pathRes = await loadBaseDir();
baseDir.value = pathRes.data;
changePath();
};
const changePath = async () => {
composeFile.value = baseDir.value + '/docker/compose/' + form.name;
};
const loadDir = async (path: string) => {
form.path = path;
};
const onDelete = (row: any) => {
@@ -477,7 +638,7 @@ const onSubmitEdit = async () => {
path: currentCompose.value.path,
content: composeContent.value,
createdBy: currentCompose.value.createdBy,
env: envStr.value ? envStr.value.split('\n') : [],
env: envs.value || [],
};
loading.value = true;
await composeUpdate(param)
@@ -496,6 +657,34 @@ const onSubmitEdit = async () => {
});
};
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
if ((form.from === 'edit' || form.from === 'template') && form.file.length === 0) {
MsgError(i18n.global.t('container.contentEmpty'));
return;
}
loading.value = true;
await testCompose(form)
.then(async (res) => {
loading.value = false;
if (res.data) {
form.taskID = newUUID();
await upCompose(form);
openTaskLog(form.taskID);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
}
})
.catch(() => {
loading.value = false;
});
});
};
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const openComposeFolder = (row: any) => {
if (row?.workdir) {
routerToFileWithPath(row.workdir);
@@ -526,4 +715,12 @@ const onOpenLog = (row: any) => {
font-size: 6px;
cursor: pointer;
}
.envTitle {
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-primary);
margin-top: 12px;
margin-bottom: 4px;
display: block;
}
</style>