Compare commits

...
Author SHA1 Message Date
ssongliu 534bd98230 style: 修改按钮组边框宽度不一致的问题 (#6706)
Refs #6704
2024-10-14 14:44:58 +08:00
ssongliu 1045f2d475 feat: 修改容器列表应用商店容器默认显示 (#6705) 2024-10-14 14:44:55 +08:00
wild_pointer c5d007e4dd fix: 修复“修改文件权限时,文件所属用户和用户组不显示”问题 (#6698) 2024-10-14 10:57:38 +08:00
zzzandzengzz01 da249a2cef feat:firewalld和ufw同时存在时,不进行操作 (#6688)
Co-authored-by: zengzz01 <zengzz01@mingyuanyun.com>
2024-10-14 10:57:33 +08:00
ssongliu f738cd42be fix: 概览页磁盘显示排除 overlay 类型 (#6672)
Refs #6637
2024-10-14 10:56:44 +08:00
ssongliu d7d15479f2 feat: 容器列表缓存是否显示应用商店容器 (#6667)
Refs #5885
2024-10-14 10:56:40 +08:00
see-more d9de82dc04 chore;删除error_code组件的body标签 (#6656) 2024-10-14 10:56:26 +08:00
zhengkunwang 2d0d3704ae feat: 反向代理文本替换增加 sub_filter_types 参数 (#6661)
Refs https://github.com/1Panel-dev/1Panel/issues/6490
2024-10-14 10:56:09 +08:00
zhengkunwang a3bad3ccf9 feat: 反代回源 SNI 支持设置 proxy_ssl_name (#6659)
Refs https://github.com/1Panel-dev/1Panel/issues/5960
2024-10-14 10:54:29 +08:00
ssongliu 41471f93f0 fix: 修改上传恢复提示信息 (#6647) 2024-10-14 10:54:11 +08:00
zhengkunwang a817beb029 feat: 自签证书支持 ipv6 (#6627)
Refs https://github.com/1Panel-dev/1Panel/issues/6623
2024-10-14 10:54:08 +08:00
ssongliu e6322ad76b style: 修改许可证即将过期样式 (#6616) 2024-09-29 14:40:52 +08:00
ssongliu df003c5fb7 feat: 移除编排删除后跳转 (#6613) 2024-09-29 10:17:04 +08:00
ssongliu 10f8e8f3c4 feat: 调整编排删除逻辑 (#6610) 2024-09-28 22:29:44 +08:00
ssongliu a750e87940 feat: Compose 环境变量编辑增加是否 1Panel 创建校验 (#6605) 2024-09-28 09:43:03 +08:00
John Bro c12f690753 fix: 重构容器编排环境变量设置逻辑 (#6600) 2024-09-27 22:34:38 +08:00
zhengkunwang 0dd51d0c35 feat: 优化日志加载 (#6593) 2024-09-27 22:34:33 +08:00
Kataick 7f627c041c flx: 修复会默认出现水平滚动条的问题 (#6564) 2024-09-25 10:50:55 +08:00
wanghe-fit2cloud 472ca884fd Revert "style:优化概览界面的布局 (#6491)"
This reverts commit 7f06c7e92e.
2024-09-25 10:50:29 +08:00
32 changed files with 313 additions and 303 deletions
+2 -1
View File
@@ -200,6 +200,7 @@ type ComposeInfo struct {
Workdir string `json:"workdir"`
Path string `json:"path"`
Containers []ComposeContainer `json:"containers"`
Env []string `json:"env"`
}
type ComposeContainer struct {
ContainerID string `json:"containerID"`
@@ -218,7 +219,7 @@ type ComposeCreate struct {
type ComposeOperation struct {
Name string `json:"name" validate:"required"`
Path string `json:"path" validate:"required"`
Operation string `json:"operation" validate:"required,oneof=up start stop down"`
Operation string `json:"operation" validate:"required,oneof=up start stop down delete"`
WithFile bool `json:"withFile"`
}
type ComposeUpdate struct {
+16 -15
View File
@@ -175,21 +175,22 @@ type WebsiteUpdateDirPermission struct {
}
type WebsiteProxyConfig struct {
ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"`
Enable bool `json:"enable" `
Cache bool `json:"cache" `
CacheTime int `json:"cacheTime" `
CacheUnit string `json:"cacheUnit"`
Name string `json:"name" validate:"required"`
Modifier string `json:"modifier"`
Match string `json:"match" validate:"required"`
ProxyPass string `json:"proxyPass" validate:"required"`
ProxyHost string `json:"proxyHost" validate:"required"`
Content string `json:"content"`
FilePath string `json:"filePath"`
Replaces map[string]string `json:"replaces"`
SNI bool `json:"sni"`
ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"`
Enable bool `json:"enable" `
Cache bool `json:"cache" `
CacheTime int `json:"cacheTime" `
CacheUnit string `json:"cacheUnit"`
Name string `json:"name" validate:"required"`
Modifier string `json:"modifier"`
Match string `json:"match" validate:"required"`
ProxyPass string `json:"proxyPass" validate:"required"`
ProxyHost string `json:"proxyHost" validate:"required"`
Content string `json:"content"`
FilePath string `json:"filePath"`
Replaces map[string]string `json:"replaces"`
SNI bool `json:"sni"`
ProxySSLName string `json:"proxySSLName"`
}
type WebsiteProxyReq struct {
+72 -143
View File
@@ -4,12 +4,11 @@ import (
"bufio"
"errors"
"fmt"
"gopkg.in/yaml.v3"
"io"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"sort"
"strings"
"time"
@@ -161,7 +160,26 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
}
BackDatas = records[start:end]
}
return int64(total), BackDatas, nil
listItem := loadEnv(BackDatas)
return int64(total), listItem, nil
}
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
for i := 0; i < len(list); i++ {
envFilePath := filepath.Join(path.Dir(list[i].Path), "1panel.env")
file, err := os.ReadFile(envFilePath)
if err != nil {
continue
}
lines := strings.Split(string(file), "\n")
for _, line := range lines {
lineItem := strings.TrimSpace(line)
if len(lineItem) != 0 && !strings.HasPrefix(lineItem, "#") {
list[i].Env = append(list[i].Env, lineItem)
}
}
}
return list
}
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
@@ -175,6 +193,9 @@ func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
if err := u.loadPath(&req); err != nil {
return false, err
}
if err := newComposeEnv(req.Path, req.Env); err != nil {
return false, err
}
cmd := exec.Command("docker-compose", "-f", req.Path, "config")
stdout, err := cmd.CombinedOutput()
if err != nil {
@@ -183,59 +204,6 @@ func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
return true, nil
}
func formatYAML(data []byte) []byte {
return []byte(strings.ReplaceAll(string(data), "\t", " "))
}
func updateDockerComposeWithEnv(req dto.ComposeCreate) error {
data, err := ioutil.ReadFile(req.Path)
if err != nil {
return fmt.Errorf("failed to read docker-compose.yml: %v", err)
}
var composeItem DockerCompose
if err := yaml.Unmarshal(data, &composeItem); err != nil {
return fmt.Errorf("failed to parse docker-compose.yml: %v", err)
}
for serviceName, service := range composeItem.Services {
envMap := make(map[string]string)
if existingEnv, exists := service["environment"].([]interface{}); exists {
for _, env := range existingEnv {
envStr := env.(string)
parts := strings.SplitN(envStr, "=", 2)
if len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
}
for _, env := range req.Env {
parts := strings.SplitN(env, "=", 2)
if len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
envVars := []string{}
for key, value := range envMap {
envVars = append(envVars, key+"="+value)
}
service["environment"] = envVars
composeItem.Services[serviceName] = service
}
if composeItem.Networks != nil {
for key := range composeItem.Networks {
composeItem.Networks[key] = map[string]interface{}{}
}
}
newData, err := yaml.Marshal(&composeItem)
if err != nil {
return fmt.Errorf("failed to marshal docker-compose.yml: %v", err)
}
formattedData := formatYAML(newData)
if err := ioutil.WriteFile(req.Path, formattedData, 0644); err != nil {
return fmt.Errorf("failed to write docker-compose.yml: %v", err)
}
return nil
}
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error) {
if cmd.CheckIllegal(req.Name, req.Path) {
return "", buserr.New(constant.ErrCmdIllegal)
@@ -260,11 +228,8 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error)
if err != nil {
return "", err
}
if len(req.Env) > 0 {
if err := updateDockerComposeWithEnv(req); err != nil {
fmt.Printf("failed to update docker-compose.yml with env: %v\n", err)
return "", err
}
if err := newComposeEnv(req.Path, req.Env); err != nil {
return "", err
}
go func() {
defer file.Close()
@@ -293,6 +258,16 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
if _, err := os.Stat(req.Path); err != nil {
return fmt.Errorf("load file with path %s failed, %v", req.Path, err)
}
if req.Operation == "delete" {
if stdout, err := compose.Operate(req.Path, "down"); err != nil {
return errors.New(string(stdout))
}
if req.WithFile {
_ = os.RemoveAll(path.Dir(req.Path))
}
_ = composeRepo.DeleteRecord(commonRepo.WithByName(req.Name))
return nil
}
if req.Operation == "up" {
if stdout, err := compose.Up(req.Path); err != nil {
return errors.New(string(stdout))
@@ -303,74 +278,6 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
}
}
global.LOG.Infof("docker-compose %s %s successful", req.Operation, req.Name)
if req.Operation == "down" {
if req.WithFile {
_ = composeRepo.DeleteRecord(commonRepo.WithByName(req.Name))
_ = os.RemoveAll(path.Dir(req.Path))
} else {
composeItem, _ := composeRepo.GetRecord(commonRepo.WithByName(req.Name))
if composeItem.Path == "" {
upMap := make(map[string]interface{})
upMap["path"] = req.Path
_ = composeRepo.UpdateRecord(req.Name, upMap)
}
}
}
return nil
}
func updateComposeWithEnv(req dto.ComposeUpdate) error {
var composeItem DockerCompose
if err := yaml.Unmarshal([]byte(req.Content), &composeItem); err != nil {
return fmt.Errorf("failed to parse docker-compose content: %v", err)
}
for serviceName, service := range composeItem.Services {
envMap := make(map[string]string)
for _, env := range req.Env {
parts := strings.SplitN(env, "=", 2)
if len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
newEnvVars := []string{}
if existingEnv, exists := service["environment"].([]interface{}); exists {
for _, env := range existingEnv {
envStr := env.(string)
parts := strings.SplitN(envStr, "=", 2)
if len(parts) == 2 {
key := parts[0]
if value, found := envMap[key]; found {
newEnvVars = append(newEnvVars, key+"="+value)
delete(envMap, key)
} else {
newEnvVars = append(newEnvVars, envStr)
}
}
}
}
for key, value := range envMap {
newEnvVars = append(newEnvVars, key+"="+value)
}
if len(newEnvVars) > 0 {
service["environment"] = newEnvVars
} else {
delete(service, "environment")
}
composeItem.Services[serviceName] = service
}
if composeItem.Networks != nil {
for key := range composeItem.Networks {
composeItem.Networks[key] = map[string]interface{}{}
}
}
newData, err := yaml.Marshal(&composeItem)
if err != nil {
return fmt.Errorf("failed to marshal docker-compose.yml: %v", err)
}
if err := ioutil.WriteFile(req.Path, newData, 0644); err != nil {
return fmt.Errorf("failed to write docker-compose.yml to path: %v", err)
}
return nil
}
@@ -382,21 +289,20 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
if err != nil {
return fmt.Errorf("load file with path %s failed, %v", req.Path, err)
}
if len(req.Env) > 0 {
if err := updateComposeWithEnv(req); err != nil {
return fmt.Errorf("failed to update docker-compose with env: %v", err)
}
} else {
file, err := os.OpenFile(req.Path, os.O_WRONLY|os.O_TRUNC, 0640)
if err != nil {
return err
}
defer file.Close()
write := bufio.NewWriter(file)
_, _ = write.WriteString(req.Content)
write.Flush()
global.LOG.Infof("docker-compose.yml %s has been replaced", req.Path)
file, err := os.OpenFile(req.Path, os.O_WRONLY|os.O_TRUNC, 0640)
if err != nil {
return err
}
defer file.Close()
write := bufio.NewWriter(file)
_, _ = write.WriteString(req.Content)
write.Flush()
global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.Path)
if err := newComposeEnv(req.Path, req.Env); err != nil {
return err
}
if stdout, err := compose.Up(req.Path); err != nil {
if err := recreateCompose(string(oldFile), req.Path); err != nil {
return fmt.Errorf("update failed when handle compose up, err: %s, recreate failed: %v", string(stdout), err)
@@ -445,3 +351,26 @@ func recreateCompose(content, path string) error {
}
return nil
}
func newComposeEnv(pathItem string, env []string) error {
if len(env) == 0 {
return nil
}
envFilePath := path.Join(path.Dir(pathItem), "1panel.env")
file, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
global.LOG.Errorf("failed to create env file: %v", err)
return err
}
defer file.Close()
for _, env := range env {
envItem := strings.TrimSpace(env)
if _, err := file.WriteString(fmt.Sprintf("%s\n", envItem)); err != nil {
global.LOG.Errorf("failed to write env to file: %v", err)
return err
}
}
global.LOG.Infof("1panel.env file successfully created or updated with env variables in %s", envFilePath)
return nil
}
+7 -6
View File
@@ -3,10 +3,6 @@ package service
import (
"encoding/json"
"fmt"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/shirou/gopsutil/v3/load"
"github.com/shirou/gopsutil/v3/mem"
"github.com/shirou/gopsutil/v3/net"
network "net"
"os"
"sort"
@@ -14,6 +10,11 @@ import (
"sync"
"time"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/shirou/gopsutil/v3/load"
"github.com/shirou/gopsutil/v3/mem"
"github.com/shirou/gopsutil/v3/net"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
@@ -268,13 +269,13 @@ func loadDiskInfo() []dto.DiskInfo {
if strings.HasPrefix(fields[6], "/snap") || len(strings.Split(fields[6], "/")) > 10 {
continue
}
if strings.TrimSpace(fields[1]) == "tmpfs" {
if strings.TrimSpace(fields[1]) == "tmpfs" || strings.TrimSpace(fields[1]) == "overlay" {
continue
}
if strings.Contains(fields[2], "K") {
continue
}
if strings.Contains(fields[6], "docker") {
if strings.Contains(fields[6], "docker") || strings.Contains(fields[6], "podman") || strings.Contains(fields[6], "containerd") || strings.HasPrefix(fields[6], "/var/lib/containers") {
continue
}
isExclude := false
+6
View File
@@ -1667,6 +1667,9 @@ func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error)
}
if req.SNI {
location.UpdateDirective("proxy_ssl_server_name", []string{"on"})
if req.ProxySSLName != "" {
location.UpdateDirective("proxy_ssl_name", []string{req.ProxySSLName})
}
} else {
location.UpdateDirective("proxy_ssl_server_name", []string{"off"})
}
@@ -1749,6 +1752,9 @@ func (w WebsiteService) GetProxies(id uint) (res []request.WebsiteProxyConfig, e
if directive.GetName() == "proxy_ssl_server_name" {
proxyConfig.SNI = directive.GetParameters()[0] == "on"
}
if directive.GetName() == "proxy_ssl_name" {
proxyConfig.ProxySSLName = directive.GetParameters()[0]
}
}
res = append(res, proxyConfig)
}
+7 -8
View File
@@ -217,15 +217,14 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
if req.Domains != "" {
domainArray := strings.Split(req.Domains, "\n")
for _, domain := range domainArray {
if !common.IsValidDomain(domain) {
err = buserr.WithName("ErrDomainFormat", domain)
return nil, err
} else {
if ipAddress := net.ParseIP(domain); ipAddress == nil {
domains = append(domains, domain)
} else {
ips = append(ips, ipAddress)
if ipAddress := net.ParseIP(domain); ipAddress == nil {
if !common.IsValidDomain(domain) {
err = buserr.WithName("ErrDomainFormat", domain)
return nil, err
}
domains = append(domains, domain)
} else {
ips = append(ips, ipAddress)
}
}
if len(domains) > 0 {
+9 -2
View File
@@ -29,10 +29,17 @@ type FirewallClient interface {
}
func NewFirewallClient() (FirewallClient, error) {
if _, err := os.Stat("/usr/sbin/firewalld"); err == nil {
_, firewalldErr := os.Stat("/usr/sbin/firewalld")
_, ufwErr := os.Stat("/usr/sbin/ufw")
if firewalldErr == nil && ufwErr == nil {
return nil, buserr.New("firewalld and ufw both found, only one firewall should be active")
}
if firewalldErr == nil {
return client.NewFirewalld()
}
if _, err := os.Stat("/usr/sbin/ufw"); err == nil {
if ufwErr == nil {
return client.NewUfw()
}
return nil, buserr.New(constant.ErrFirewall)
@@ -234,11 +234,13 @@ func (l *Location) AddSubFilter(subFilters map[string]string) {
}
l.UpdateDirective("proxy_set_header", []string{"Accept-Encoding", `""`})
l.UpdateDirective("sub_filter_once", []string{"off"})
l.UpdateDirective("sub_filter_types", []string{"*"})
}
func (l *Location) RemoveSubFilter() {
l.RemoveDirective("sub_filter", []string{})
l.RemoveDirective("proxy_set_header", []string{"Accept-Encoding", `""`})
l.RemoveDirective("sub_filter_once", []string{"off"})
l.RemoveDirective("sub_filter_types", []string{"*"})
l.Replaces = nil
}
+1
View File
@@ -11,4 +11,5 @@ location ^~ /test {
add_header X-Cache $upstream_cache_status;
add_header Cache-Control no-cache;
proxy_ssl_server_name off;
proxy_ssl_name $proxy_host;
}
+3
View File
@@ -241,6 +241,8 @@ export namespace Container {
path: string;
containers: Array<ComposeContainer>;
expand: boolean;
envStr: string;
env: Array<string>;
}
export interface ComposeContainer {
name: string;
@@ -268,6 +270,7 @@ export namespace Container {
path: string;
content: string;
env: Array<string>;
createdBy: string;
}
export interface TemplateCreate {
+1
View File
@@ -174,6 +174,7 @@ export namespace Setting {
}
export interface LicenseStatus {
productPro: string;
trial: boolean;
status: string;
}
}
+2
View File
@@ -373,6 +373,8 @@ export namespace Website {
content?: string;
proxyAddress?: string;
proxyProtocol?: string;
sni: boolean;
proxySSLName: string;
}
export interface ProxReplace {
@@ -108,6 +108,9 @@ const showBack = computed(() => {
.el-button + .el-button {
margin: 0 !important;
}
.el-button-group .el-button {
margin: 0 !important;
}
}
.content-container_form {
+4 -4
View File
@@ -1,5 +1,5 @@
<template>
<div v-loading="isLoading">
<div v-loading="firstLoading">
<div v-if="defaultButton">
<el-checkbox border v-model="tailLog" class="float-left" @change="changeTail(false)">
{{ $t('commons.button.watch') }}
@@ -78,6 +78,7 @@ const minPage = ref(1);
const maxPage = ref(1);
const logs = ref([]);
const isLoading = ref(false);
const firstLoading = ref(true);
const readReq = reactive({
id: 0,
@@ -129,6 +130,7 @@ const getContent = (pre: boolean) => {
}
isLoading.value = true;
ReadByLine(readReq).then((res) => {
firstLoading.value = false;
if (!end.value && res.data.end) {
lastLogs.value = [...logs.value];
}
@@ -193,7 +195,7 @@ const getContent = (pre: boolean) => {
maxPage.value = res.data.total;
minPage.value = res.data.total;
}
if (logs.value && logs.value.length > 3000) {
if (logs.value && logs.value.length > 1000) {
logs.value.splice(0, readReq.pageSize);
if (minPage.value > 1) {
minPage.value--;
@@ -278,8 +280,6 @@ const init = () => {
}
readReq.latest = true;
search();
nextTick(() => {});
};
const clearLog = (): void => {
@@ -1,6 +1,6 @@
<template>
<el-card class="router_card">
<div class="flex w-full justify-between items-center md:justify-between md:flex-row">
<div class="flex w-full flex-col md:justify-between md:flex-row">
<el-radio-group v-model="activeName" @change="handleChange">
<el-radio-button
class="router_card_button"
@@ -76,7 +76,6 @@ onMounted(() => {
<style lang="scss">
.router_card {
--el-card-padding: 0;
width: 100%;
.el-card__body {
display: flex;
justify-content: space-between;
+2 -2
View File
@@ -211,8 +211,8 @@ const onRecover = async (row: File.File) => {
currentRow.value = row;
if (type.value !== 'app' && type.value !== 'website') {
ElMessageBox.confirm(
i18n.global.t('commons.msg.backupHelper', [name.value + '( ' + detailName.value + ' )']),
i18n.global.t('commons.button.backup'),
i18n.global.t('commons.msg.recoverHelper', [row.name]),
i18n.global.t('commons.button.recover'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
+4 -2
View File
@@ -655,7 +655,7 @@ const message = {
privilegedHelper:
'Allows the container to perform certain privileged operations on the host, which may increase container risks. Use with caution!',
editComposeHelper:
'The environment variables manually entered in the menu will override existing variables with the same name. If they do not exist, they will be added.',
'Note: The environment variables set will be written to the 1panel.env file by default.\nIf you want to use these parameters in the container, you also need to manually add an env_file reference in the compose file.',
upgradeHelper: 'Repository Name/Image Name: Image Version',
upgradeWarning2:
@@ -772,7 +772,7 @@ const message = {
deleteFile: 'Delete file',
allDelete: 'Permanently Delete',
deleteComposeHelper:
'1. Delete container orchestration records \n2. Delete all container orchestration files, including configuration and persistent files',
'Delete all files related to container compose, including configuration files and persistent files. Please proceed with caution!',
deleteCompose: '" Delete this composition.',
apps: 'Apps',
local: 'Local',
@@ -792,6 +792,8 @@ const message = {
composeDetailHelper:
'The compose is created external to 1Panel. The start and stop operations are not supported.',
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?',
setting: 'Setting',
operatorStatusHelper: 'This action will {0} Docker service, do you want to continue?',
+4 -2
View File
@@ -629,7 +629,8 @@ const message = {
emptyUser: '為空時,將使用容器默認的用戶登錄',
privileged: '特權模式',
privilegedHelper: '允許容器在主機上執行某些特權操作,可能會增加容器風險,請謹慎開啟!',
editComposeHelper: '在菜單中手動輸入的環境變量會覆蓋原有的同名變量,若不存在則新增。',
editComposeHelper:
'注意:設置的環境變數會默認寫入 1panel.env 文件。\n若需在容器中使用這些參數,還需在 compose 文件中手動添加 env_file 引用。',
upgradeHelper: '倉庫名稱/鏡像名稱:鏡像版本',
upgradeWarning2: '升級操作需要重建容器,任何未持久化的數據將會丟失,是否繼續?',
@@ -741,7 +742,7 @@ const message = {
composeHelper: '通過 1Panel 編輯或者模版創建的編排,將保存在 {0}/docker/compose 路徑下',
deleteFile: '刪除文件',
allDelete: '徹底刪除',
deleteComposeHelper: '1. 刪除容器編排記錄 \n2. 刪除容器編排的所有文件,包括配置文件和持久化文件',
deleteComposeHelper: '刪除容器編排的所有文件,包括配置文件和持久化文件,請謹慎操作!',
deleteCompose: '" 刪除此編排',
apps: '應用商店',
local: '本地',
@@ -760,6 +761,7 @@ const message = {
up: '啟動',
composeDetailHelper: '該 compose 為 1Panel 編排外部創建。暫不支持啟停操作。',
composeOperatorHelper: '將對 {0} 進行 {1} 操作,是否繼續?',
composeDownHelper: '將停止並刪除 {0} 編排下所有容器及網絡,是否繼續?',
setting: '配置',
operatorStatusHelper: '此操作將{0}Docker 服務,是否繼續?',
+4 -3
View File
@@ -632,8 +632,8 @@ const message = {
emptyUser: '为空时,将使用容器默认的用户登录',
privileged: '特权模式',
privilegedHelper: '允许容器在主机上执行某些特权操作,可能会增加容器风险,谨慎开启!',
editComposeHelper: '菜单中手动输入的环境变量会覆盖原有的同名变量,如果不存在则新增',
editComposeHelper:
'注意:设置的环境变量会默认写入 1panel.env 文件。\n如需在容器中使用这些参数,还需在 compose 文件中手动添加 env_file 引用。',
upgradeHelper: '仓库名称/镜像名称:镜像版本',
upgradeWarning2: '升级操作需要重建容器,任何未持久化的数据将会丢失,是否继续?',
oldImage: '当前镜像',
@@ -743,7 +743,7 @@ const message = {
composeHelper: '通过 1Panel 编辑或者模版创建的编排,将保存在 {0}/docker/compose 路径下',
deleteFile: '删除文件',
allDelete: '彻底删除',
deleteComposeHelper: '1. 删除容器编排记录 \n2. 删除容器编排的所有文件,包括配置文件和持久化文件',
deleteComposeHelper: '删除容器编排的所有文件,包括配置文件和持久化文件,请谨慎操作!',
deleteCompose: '" 删除此编排',
apps: '应用商店',
local: '本地',
@@ -762,6 +762,7 @@ const message = {
up: '启动',
composeDetailHelper: '该 compose 为 1Panel 编排外部创建。暂不支持启停操作。',
composeOperatorHelper: '将对 {0} 进行 {1} 操作,是否继续?',
composeDownHelper: '将停止并删除 {0} 编排下所有容器及网络,是否继续?',
setting: '配置',
operatorStatusHelper: '此操作将{0}Docker 服务,是否继续?',
+1
View File
@@ -34,6 +34,7 @@ export interface GlobalState {
defaultNetwork: string;
isProductPro: boolean;
isTrial: boolean;
productProExpires: number;
errStatus: string;
+1
View File
@@ -38,6 +38,7 @@ const GlobalStore = defineStore({
defaultNetwork: 'all',
isProductPro: false,
isTrial: false,
productProExpires: 0,
errStatus: '',
+2
View File
@@ -61,6 +61,7 @@ export async function loadProductProFromDB() {
res.data.status === 'Enable' || res.data.status === 'Lost01' || res.data.status === 'Lost02';
if (globalStore.isProductPro) {
globalStore.productProExpires = Number(res.data.productPro);
globalStore.isTrial = res.data.trial;
}
}
switchTheme();
@@ -81,6 +82,7 @@ export async function getXpackSettingForTheme() {
res.data.status === 'Enable' || res.data.status === 'Lost01' || res.data.status === 'Lost02';
if (globalStore.isProductPro) {
globalStore.productProExpires = Number(res.data.productPro);
globalStore.isTrial = res.data.trial;
}
if (!globalStore.isProductPro) {
globalStore.isProductPro = false;
@@ -66,7 +66,7 @@
placeholder="#Define or paste the content of your docker-compose file here"
:indent-with-tab="true"
:tabSize="4"
style="width: 100%; height: calc(100vh - 376px)"
style="width: 100%; height: calc(100vh - 400px)"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
@@ -94,6 +94,18 @@
v-model="form.envStr"
/>
</el-form-item>
<span class="input-help whitespace-break-spaces">{{ $t('container.editComposeHelper') }}</span>
<codemirror
v-model="form.envFileContent"
:autofocus="true"
:indent-with-tab="true"
:tabSize="4"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
></codemirror>
</el-form>
</el-col>
</el-row>
@@ -153,6 +165,7 @@ const form = reactive({
template: null as number,
env: [],
envStr: '',
envFileContent: `env_file:\n - 1panel.env`,
});
const rules = reactive({
name: [Rules.requiredInput, Rules.imageName],
@@ -7,7 +7,7 @@
>
<el-form ref="deleteForm" v-loading="loading">
<el-form-item>
<el-checkbox v-model="deleteFile" :label="$t('container.allDelete')" />
<el-checkbox v-model="deleteFile" :label="$t('container.deleteFile')" />
<span class="input-help whitespace-break-spaces">
{{ $t('container.deleteComposeHelper') }}
</span>
@@ -69,7 +69,7 @@ const submit = async () => {
let params = {
name: composeName.value,
path: composePath.value,
operation: 'down',
operation: 'delete',
withFile: deleteFile.value,
};
await composeOperator(params)
@@ -143,7 +143,6 @@ const dialogContainerLogRef = ref();
const opRef = ref();
const emit = defineEmits<{ (e: 'back'): void }>();
interface DialogProps {
createdBy: string;
name: string;
@@ -260,15 +259,18 @@ const onOperate = async (op: string) => {
};
const onComposeOperate = async (operation: string) => {
ElMessageBox.confirm(
i18n.global.t('container.composeOperatorHelper', [composeName.value, i18n.global.t('container.' + operation)]),
i18n.global.t('container.' + operation),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(async () => {
let mes =
operation === 'down'
? i18n.global.t('container.composeDownHelper', [composeName.value])
: i18n.global.t('container.composeOperatorHelper', [
composeName.value,
i18n.global.t('container.' + operation),
]);
ElMessageBox.confirm(mes, i18n.global.t('container.' + operation), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
let params = {
name: composeName.value,
path: composePath.value,
@@ -280,11 +282,7 @@ const onComposeOperate = async (operation: string) => {
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
if (operation === 'down') {
emit('back');
} else {
search();
}
search();
})
.catch(() => {
loading.value = false;
@@ -19,7 +19,7 @@
placeholder="#Define or paste the content of your docker-compose file here"
:indent-with-tab="true"
:tabSize="4"
style="width: 100%; height: calc(100vh - 175px)"
:style="{ width: '100%', height: `calc(100vh - ${loadHeight()})` }"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
@@ -28,15 +28,34 @@
v-model="content"
/>
</el-form-item>
<el-form-item :label="$t('container.env')" prop="environmentStr">
<el-input
type="textarea"
:placeholder="$t('container.tagHelper')"
:rows="3"
v-model="environmentStr"
/>
</el-form-item>
<span class="input-help">{{ $t('container.editComposeHelper') }}</span>
<div v-if="createdBy === '1Panel'">
<el-form-item
:label="$t('container.env')"
prop="environmentStr"
v-if="createdBy === '1Panel'"
>
<el-input
type="textarea"
:placeholder="$t('container.tagHelper')"
:rows="3"
v-model="environmentStr"
/>
</el-form-item>
<span class="input-help whitespace-break-spaces">
{{ $t('container.editComposeHelper') }}
</span>
<codemirror
v-model="envFileContent"
:autofocus="true"
:indent-with-tab="true"
:tabSize="4"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
></codemirror>
</div>
</el-form>
</el-col>
</el-row>
@@ -64,6 +83,7 @@ import { MsgSuccess } from '@/utils/message';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { ElForm } from 'element-plus';
const emit = defineEmits<{ (e: 'search'): void }>();
const loading = ref(false);
const composeVisible = ref(false);
const extensions = [javascript(), oneDark];
@@ -71,6 +91,9 @@ const path = ref();
const content = ref();
const name = ref();
const environmentStr = ref();
const environmentEnv = ref();
const createdBy = ref();
const envFileContent = `env_file:\n - 1panel.env`;
const onSubmitEdit = async () => {
const param = {
@@ -78,6 +101,7 @@ const onSubmitEdit = async () => {
path: path.value,
content: content.value,
env: environmentStr.value,
createdBy: createdBy.value,
};
if (environmentStr.value != undefined) {
param.env = environmentStr.value.split('\n');
@@ -88,6 +112,7 @@ const onSubmitEdit = async () => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
composeVisible.value = false;
emit('search');
})
.catch(() => {
loading.value = false;
@@ -98,14 +123,22 @@ interface DialogProps {
name: string;
path: string;
content: string;
env: Array<string>;
envStr: string;
createdBy: string;
}
const loadHeight = () => {
return createdBy.value === '1Panel' ? '300px' : '200px';
};
const acceptParams = (props: DialogProps): void => {
composeVisible.value = true;
path.value = props.path;
name.value = props.name;
content.value = props.content;
environmentStr.value = '';
createdBy.value = props.createdBy;
environmentEnv.value = props.env || [];
environmentStr.value = environmentEnv.value.join('\n');
};
const handleClose = () => {
composeVisible.value = false;
@@ -1,7 +1,7 @@
<template>
<div v-loading="loading">
<div v-show="isOnDetail">
<ComposeDetail @back="backList" ref="composeDetailRef" />
<ComposeDetail ref="composeDetailRef" />
</div>
<el-card v-if="dockerStatus != 'Running'" class="mask-prompt">
<span>{{ $t('container.serviceUnavailable') }}</span>
@@ -83,7 +83,7 @@
</template>
</LayoutContent>
<EditDialog ref="dialogEditRef" />
<EditDialog @search="search" ref="dialogEditRef" />
<CreateDialog @search="search" ref="dialogRef" />
<DeleteDialog @search="search" ref="dialogDelRef" />
</div>
@@ -185,10 +185,6 @@ const getContainerStatus = (containers) => {
return i18n.global.t('container.running') + ` (${runningCount}/${totalCount})`;
}
};
const backList = async () => {
isOnDetail.value = false;
search();
};
const dialogRef = ref();
const onOpenDialog = async () => {
@@ -211,6 +207,8 @@ const onEdit = async (row: Container.ComposeInfo) => {
name: row.name,
path: row.path,
content: res.data,
env: row.env,
createdBy: row.createdBy,
};
dialogEditRef.value!.acceptParams(params);
};
@@ -369,7 +369,7 @@ const dialogUpgradeRef = ref();
const dialogCommitRef = ref();
const dialogPortJumpRef = ref();
const opRef = ref();
const includeAppStore = ref(true);
const includeAppStore = ref();
const columns = ref([]);
const dockerStatus = ref('Running');
@@ -423,6 +423,7 @@ const dialogRenameRef = ref();
const dialogPruneRef = ref();
const search = async (column?: any) => {
localStorage.setItem('includeAppStore', includeAppStore.value ? 'true' : 'false');
let filterItem = props.filters ? props.filters : '';
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
@@ -767,6 +768,8 @@ const buttons = [
];
onMounted(() => {
let includeItem = localStorage.getItem('includeAppStore');
includeAppStore.value = !includeItem || includeItem === 'true';
loadStatus();
});
</script>
+68 -74
View File
@@ -42,37 +42,39 @@
<el-row :gutter="20" style="margin-top: 20px">
<el-col :xs="24" :sm="24" :md="16" :lg="16" :xl="16">
<CardWithHeader :header="$t('home.overview')">
<CardWithHeader :header="$t('home.overview')" height="166px">
<template #body>
<div class="h-overview">
<div>
<span>{{ $t('menu.website') }}</span>
<div class="count">
<span @click="goRouter('/websites')">{{ baseInfo?.websiteNumber }}</span>
</div>
</div>
<div>
<span>{{ $t('menu.database') }} - {{ $t('database.all') }}</span>
<div class="count">
<span @click="goRouter('/databases')">{{ baseInfo?.databaseNumber }}</span>
</div>
</div>
<div>
<span>{{ $t('menu.cronjob') }}</span>
<div class="count">
<span @click="goRouter('/cronjobs')">
{{ baseInfo?.cronjobNumber }}
</span>
</div>
</div>
<div>
<span>{{ $t('home.appInstalled') }}</span>
<div class="count">
<span @click="goRouter('/apps/installed')">
{{ baseInfo?.appInstalledNumber }}
</span>
</div>
</div>
<el-row>
<el-col :span="6">
<span>{{ $t('menu.website') }}</span>
<div class="count">
<span @click="goRouter('/websites')">{{ baseInfo?.websiteNumber }}</span>
</div>
</el-col>
<el-col :span="6">
<span>{{ $t('menu.database') }} - {{ $t('database.all') }}</span>
<div class="count">
<span @click="goRouter('/databases')">{{ baseInfo?.databaseNumber }}</span>
</div>
</el-col>
<el-col :span="6">
<span>{{ $t('menu.cronjob') }}</span>
<div class="count">
<span @click="goRouter('/cronjobs')">
{{ baseInfo?.cronjobNumber }}
</span>
</div>
</el-col>
<el-col :span="6">
<span>{{ $t('home.appInstalled') }}</span>
<div class="count">
<span @click="goRouter('/apps/installed')">
{{ baseInfo?.appInstalledNumber }}
</span>
</div>
</el-col>
</el-row>
</div>
</template>
</CardWithHeader>
@@ -83,44 +85,42 @@
</CardWithHeader>
<CardWithHeader :header="$t('menu.monitor')" style="margin-top: 20px; margin-bottom: 20px">
<template #header-r>
<div
class="float-right flex flex-col items-end flex-wrap sm:items-center gap-2 sm:flex-row sm:gap-4"
<el-radio-group
style="float: right; margin-left: 5px"
v-model="chartOption"
@change="changeOption"
>
<el-radio-group v-model="chartOption" @change="changeOption">
<el-radio-button value="network">{{ $t('home.network') }}</el-radio-button>
<el-radio-button value="io">{{ $t('home.io') }}</el-radio-button>
</el-radio-group>
<div>
<el-select
v-if="chartOption === 'network'"
@change="onLoadBaseInfo(false, 'network')"
v-model="searchInfo.netOption"
class="p-w-200 float-right"
>
<template #prefix>{{ $t('home.networkCard') }}</template>
<el-option
v-for="item in netOptions"
:key="item"
:label="item == 'all' ? $t('commons.table.all') : item"
:value="item"
/>
</el-select>
<el-select
v-if="chartOption === 'io'"
v-model="searchInfo.ioOption"
@change="onLoadBaseInfo(false, 'io')"
class="p-w-200 float-right"
>
<template #prefix>{{ $t('home.disk') }}</template>
<el-option
v-for="item in ioOptions"
:key="item"
:label="item == 'all' ? $t('commons.table.all') : item"
:value="item"
/>
</el-select>
</div>
</div>
<el-radio-button value="network">{{ $t('home.network') }}</el-radio-button>
<el-radio-button value="io">{{ $t('home.io') }}</el-radio-button>
</el-radio-group>
<el-select
v-if="chartOption === 'network'"
@change="onLoadBaseInfo(false, 'network')"
v-model="searchInfo.netOption"
class="p-w-200 float-right"
>
<template #prefix>{{ $t('home.networkCard') }}</template>
<el-option
v-for="item in netOptions"
:key="item"
:label="item == 'all' ? $t('commons.table.all') : item"
:value="item"
/>
</el-select>
<el-select
v-if="chartOption === 'io'"
v-model="searchInfo.ioOption"
@change="onLoadBaseInfo(false, 'io')"
class="p-w-200 float-right"
>
<template #prefix>{{ $t('home.disk') }}</template>
<el-option
v-for="item in ioOptions"
:key="item"
:label="item == 'all' ? $t('commons.table.all') : item"
:value="item"
/>
</el-select>
</template>
<template #body>
<div style="position: relative; margin-top: 20px">
@@ -685,20 +685,14 @@ onBeforeUnmount(() => {
<style lang="scss">
.h-overview {
text-align: center;
display: flex;
align-items: center;
justify-content: space-around;
flex-direction: row;
span:first-child {
font-size: 14px;
color: var(--el-text-color-regular);
}
@media only screen and (max-width: 1300px) {
display: grid;
gap: 1rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
span:first-child {
font-size: 16px;
font-size: 12px;
color: var(--el-text-color-regular);
}
}
@@ -81,8 +81,8 @@ const acceptParams = (props: BatchRoleProps) => {
addForm.paths.push(file.path);
});
addForm.mode = Number.parseInt(String(props.files[0].mode), 8);
addForm.group = props.files[0].group;
addForm.user = props.files[0].user;
addForm.group = props.files[0].group || props.files[0].gid;
addForm.user = props.files[0].user || props.files[0].uid;
addForm.sub = true;
mode.value = String(props.files[0].mode);
@@ -201,6 +201,7 @@ const search = async () => {
hasLicense.value = true;
if (globalStore.isProductPro) {
globalStore.productProExpires = Number(res.data.productPro);
globalStore.isTrial = res.data.trial;
}
license.licenseName = res.data.licenseName;
license.message = res.data.message;
@@ -31,6 +31,9 @@
<el-switch v-model="proxy.sni"></el-switch>
<span class="input-help">{{ $t('website.sniHelper') }}</span>
</el-form-item>
<el-form-item label="proxy_ssl_name" prop="proxySSLName" v-if="proxy.sni">
<el-input v-model.trim="proxy.proxySSLName"></el-input>
</el-form-item>
<el-form-item :label="$t('website.cacheTime')" prop="cacheTime" v-if="proxy.cache">
<el-input v-model.number="proxy.cacheTime" maxlength="15">
<template #append>
@@ -47,7 +50,7 @@
</el-form-item>
<el-row :gutter="10">
<el-col :span="12">
<el-form-item :label="$t('website.proxyPass')" prop="proxyPass">
<el-form-item :label="$t('website.proxyPass')" prop="proxyAddress">
<el-input
v-model.trim="proxy.proxyAddress"
:placeholder="$t('website.proxyHelper')"
@@ -139,6 +142,7 @@ const rules = ref({
cacheTime: [Rules.requiredInput, checkNumberRange(1, 65535)],
proxyPass: [Rules.requiredInput],
proxyHost: [Rules.requiredInput],
proxyAddress: [Rules.requiredInput],
});
const open = ref(false);
const loading = ref(false);
@@ -159,6 +163,8 @@ const initData = (): Website.ProxyConfig => ({
replaces: {},
proxyAddress: '',
proxyProtocol: 'http://',
sni: false,
proxySSLName: '$proxy_host',
});
let proxy = ref(initData());
const replaces = ref<any>([]);