diff --git a/agent/app/service/container_compose.go b/agent/app/service/container_compose.go index 374c9b5cbd16..d7a584817976 100644 --- a/agent/app/service/container_compose.go +++ b/agent/app/service/container_compose.go @@ -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 } diff --git a/frontend/src/components/label/index.vue b/frontend/src/components/label/index.vue new file mode 100644 index 000000000000..ed3296928802 --- /dev/null +++ b/frontend/src/components/label/index.vue @@ -0,0 +1,79 @@ + + + diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index f503960ebbb3..bc91a9cf2ede 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -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', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index cf20c1811fa4..0ce135a5f287 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -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?', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 9042e2f50fe5..d4ed6bca632b 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -984,6 +984,9 @@ const message = { composeOperatorHelper: '{1}操作は{0}で実行されます。続けたいですか?', composeDownHelper: 'これにより、{0}構成の下のすべてのコンテナとネットワークが停止して削除されます。続けたいですか?', + composeEnvHelper1: '既存の環境変数を編集するにはダブルクリックしてください。', + composeEnvHelper2: + 'このオーケストレーションは1Panelアプリストアで作成されました。インストール済みアプリケーションで環境変数を変更してください。', setting: '設定|設定', operatorStatusHelper: 'これは「{0}」Dockerサービスになります。続けたいですか?', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 24520ccb21a9..3cd09a29f463 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -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}" 합니다. 계속 하시겠습니까?', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 0536fce1a869..3238ca98875c 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -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?', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 93ceed1c2cc3..0d2d7f1f45cd 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -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?', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index b5e0f4bffaac..82c5b0a3aa70 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -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. Хотите продолжить?', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index ac73eea84d9f..4f80aacb95be 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -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', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 59b990c82498..08a15285ec58 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -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: '去修改', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 9a99475b2bb8..a52ef728143f 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -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: '去修改', diff --git a/frontend/src/views/container/compose/create/index.vue b/frontend/src/views/container/compose/create/index.vue deleted file mode 100644 index a245e12dac38..000000000000 --- a/frontend/src/views/container/compose/create/index.vue +++ /dev/null @@ -1,241 +0,0 @@ - - - diff --git a/frontend/src/views/container/compose/index.vue b/frontend/src/views/container/compose/index.vue index e7c034ea5cd2..2c31ed5b3f52 100644 --- a/frontend/src/views/container/compose/index.vue +++ b/frontend/src/views/container/compose/index.vue @@ -107,7 +107,7 @@ - + {{ $t('container.compose') }} {{ $t('commons.button.log') }} - +
-
- - - - - {{ $t('container.editComposeHelper') }} - - -
- + {{ $t('container.env') }} +
+ + + + + {{ $t('commons.button.edit') }} + {{ $t('container.pathSelect') }} + {{ $t('container.composeTemplate') }} + + + + + + + + + + + + + + + + + + + {{ $t('container.composePathHelper', [composeFile]) }} + + + +
+ +
+
+ {{ $t('container.env') }} +
+ + + {{ $t('commons.button.save') }} + +
- + + @@ -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([]); const loading = ref(false); @@ -312,10 +384,7 @@ const detailLoading = ref(false); const currentCompose = ref(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([]); +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; +const formRef = ref(); +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; +}