mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-23 08:00:55 +00:00
feat: Container compose supports .env file operations (#11274)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -984,6 +984,9 @@ const message = {
|
||||
composeOperatorHelper: '{1}操作は{0}で実行されます。続けたいですか?',
|
||||
composeDownHelper:
|
||||
'これにより、{0}構成の下のすべてのコンテナとネットワークが停止して削除されます。続けたいですか?',
|
||||
composeEnvHelper1: '既存の環境変数を編集するにはダブルクリックしてください。',
|
||||
composeEnvHelper2:
|
||||
'このオーケストレーションは1Panelアプリストアで作成されました。インストール済みアプリケーションで環境変数を変更してください。',
|
||||
|
||||
setting: '設定|設定',
|
||||
operatorStatusHelper: 'これは「{0}」Dockerサービスになります。続けたいですか?',
|
||||
|
||||
@@ -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}" 합니다. 계속 하시겠습니까?',
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -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. Хотите продолжить?',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '去修改',
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user