Compare commits

...

18 Commits

Author SHA1 Message Date
刘祥超
d045f0526f 文件缓存策略支持二级缓存(内存 | 文件) 2021-03-02 19:38:13 +08:00
刘祥超
0766ec9d5a WAF动作增加显示HTML内容 2021-02-26 16:32:07 +08:00
刘祥超
4511b48382 可以设置管理界面和用户界面的浏览器图标和Logo 2021-02-25 20:54:30 +08:00
刘祥超
151e58fbb9 优化界面 2021-02-25 19:07:58 +08:00
刘祥超
c833ac2e96 因为健康检查下线的节点可以手动恢复上线 2021-02-24 19:27:33 +08:00
刘祥超
99983330a3 集群健康检查URL中可以输入主机名 2021-02-24 16:20:04 +08:00
刘祥超
a14e81a28f 优化界面显示 2021-02-24 15:01:45 +08:00
刘祥超
fc714a15a3 相关统计图表中X坐标显示所有标签 2021-02-24 11:11:41 +08:00
刘祥超
8a8f9d2912 修复重新加载RPC连接时可能死锁的问题 2021-02-24 10:44:53 +08:00
刘祥超
58a0521605 自动更新API节点配置 2021-02-24 09:00:12 +08:00
刘祥超
e129b52f1c 更新版本号 2021-02-24 08:46:05 +08:00
刘祥超
5cefd900a4 终端统计中显示系统和浏览器的版本号 2021-02-07 09:23:55 +08:00
刘祥超
857dc70b4d 修复一个流量格式化错误 2021-02-07 09:16:58 +08:00
刘祥超
e3036025bc 修复反向代理配置组件addHeaders错误 2021-02-07 09:07:18 +08:00
刘祥超
76035dc3c2 创建或修改服务域名时检查域名是否重复 2021-02-06 21:52:00 +08:00
刘祥超
22c55d0b83 修改节点分组的节点数显示错误 2021-02-06 19:50:00 +08:00
刘祥超
9780937a1f 修改版本号 2021-02-06 19:49:36 +08:00
刘祥超
b4eb6e92e0 优化交互 2021-02-06 19:40:29 +08:00
48 changed files with 1042 additions and 267 deletions

View File

@@ -1,7 +1,7 @@
package teaconst package teaconst
const ( const (
Version = "0.0.10" Version = "0.0.12"
ProductName = "Edge Admin" ProductName = "Edge Admin"
ProcessName = "edge-admin" ProcessName = "edge-admin"

View File

@@ -342,6 +342,12 @@ func (this *RPCClient) APIContext(apiNodeId int64) context.Context {
return ctx return ctx
} }
// 修改配置
func (this *RPCClient) UpdateConfig(config *configs.APIConfig) error {
this.apiConfig = config
return this.init()
}
// 初始化 // 初始化
func (this *RPCClient) init() error { func (this *RPCClient) init() error {
// 重新连接 // 重新连接
@@ -369,6 +375,8 @@ func (this *RPCClient) init() error {
if len(conns) == 0 { if len(conns) == 0 {
return errors.New("[RPC]no available endpoints") return errors.New("[RPC]no available endpoints")
} }
// 这里不需要加锁因为会和pickConn冲突
this.conns = conns this.conns = conns
return nil return nil
} }

View File

@@ -0,0 +1,92 @@
package tasks
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
"github.com/TeaOSLab/EdgeAdmin/internal/events"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/logs"
"sort"
"strings"
"time"
)
func init() {
events.On(events.EventStart, func() {
task := NewSyncAPINodesTask()
go task.Start()
})
}
// API节点同步任务
type SyncAPINodesTask struct {
}
func NewSyncAPINodesTask() *SyncAPINodesTask {
return &SyncAPINodesTask{}
}
func (this *SyncAPINodesTask) Start() {
ticker := time.NewTicker(5 * time.Minute)
if Tea.IsTesting() {
// 快速测试
ticker = time.NewTicker(1 * time.Minute)
}
for range ticker.C {
err := this.Loop()
if err != nil {
logs.Println("[TASK][SYNC_API_NODES]" + err.Error())
}
}
}
func (this *SyncAPINodesTask) Loop() error {
// 获取所有可用的节点
rpcClient, err := rpc.SharedRPC()
if err != nil {
return err
}
resp, err := rpcClient.APINodeRPC().FindAllEnabledAPINodes(rpcClient.Context(0), &pb.FindAllEnabledAPINodesRequest{})
if err != nil {
return err
}
newEndpoints := []string{}
for _, node := range resp.Nodes {
if !node.IsOn {
continue
}
newEndpoints = append(newEndpoints, node.AccessAddrs...)
}
// 和现有的对比
config, err := configs.LoadAPIConfig()
if err != nil {
return err
}
if this.isSame(newEndpoints, config.RPC.Endpoints) {
return nil
}
// 修改RPC对象配置
config.RPC.Endpoints = newEndpoints
err = rpcClient.UpdateConfig(config)
if err != nil {
return err
}
// 保存到文件
err = config.WriteFile(Tea.ConfigFile("api.yaml"))
if err != nil {
return err
}
return nil
}
func (this *SyncAPINodesTask) isSame(endpoints1 []string, endpoints2 []string) bool {
sort.Strings(endpoints1)
sort.Strings(endpoints2)
return strings.Join(endpoints1, "&") == strings.Join(endpoints2, "&")
}

View File

@@ -0,0 +1,15 @@
package tasks
import (
_ "github.com/iwind/TeaGo/bootstrap"
"testing"
)
func TestSyncAPINodesTask_Loop(t *testing.T) {
task := NewSyncAPINodesTask()
err := task.Loop()
if err != nil {
t.Fatal(err)
}
t.Log("ok")
}

View File

@@ -35,7 +35,7 @@ func FormatBits(bits int64) string {
} else if bits < 1000*1000*1000 { } else if bits < 1000*1000*1000 {
return fmt.Sprintf("%.2fMB", float64(bits)/1000/1000) return fmt.Sprintf("%.2fMB", float64(bits)/1000/1000)
} else if bits < 1000*1000*1000*1000 { } else if bits < 1000*1000*1000*1000 {
return fmt.Sprintf("%.2fGB", float64(bits)/1000/10001000) return fmt.Sprintf("%.2fGB", float64(bits)/1000/1000/1000)
} else { } else {
return fmt.Sprintf("%.2fPB", float64(bits)/1000/1000/1000/1000) return fmt.Sprintf("%.2fPB", float64(bits)/1000/1000/1000/1000)
} }

View File

@@ -36,6 +36,7 @@ func init() {
Get("/node/logs", new(node.LogsAction)). Get("/node/logs", new(node.LogsAction)).
Post("/node/start", new(node.StartAction)). Post("/node/start", new(node.StartAction)).
Post("/node/stop", new(node.StopAction)). Post("/node/stop", new(node.StopAction)).
Post("/node/up", new(node.UpAction)).
// 分组相关 // 分组相关
Get("/groups", new(groups.IndexAction)). Get("/groups", new(groups.IndexAction)).

View File

@@ -0,0 +1,28 @@
package node
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
// 手动上线
type UpAction struct {
actionutils.ParentAction
}
func (this *UpAction) RunPost(params struct {
NodeId int64
}) {
defer this.CreateLogInfo("手动上线节点 %d", params.NodeId)
_, err := this.RPC().NodeRPC().UpdateNodeUp(this.AdminContext(), &pb.UpdateNodeUpRequest{
NodeId: params.NodeId,
IsUp: true,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -43,6 +43,9 @@ func (this *CreatePopupAction) RunPost(params struct {
// http api // http api
HttpAPIURL string HttpAPIURL string
// html
HtmlContent string
Must *actions.Must Must *actions.Must
CSRF *actionutils.CSRF CSRF *actionutils.CSRF
}) { }) {
@@ -90,6 +93,13 @@ func (this *CreatePopupAction) RunPost(params struct {
actionParams = &firewallconfigs.FirewallActionHTTPAPIConfig{ actionParams = &firewallconfigs.FirewallActionHTTPAPIConfig{
URL: params.HttpAPIURL, URL: params.HttpAPIURL,
} }
case firewallconfigs.FirewallActionTypeHTML:
params.Must.
Field("htmlContent", params.HtmlContent).
Require("请输入HTML内容")
actionParams = &firewallconfigs.FirewallActionHTMLConfig{
Content: params.HtmlContent,
}
default: default:
this.Fail("选择的类型'" + params.Type + "'暂时不支持") this.Fail("选择的类型'" + params.Type + "'暂时不支持")
} }

View File

@@ -72,6 +72,9 @@ func (this *UpdatePopupAction) RunPost(params struct {
// http api // http api
HttpAPIURL string HttpAPIURL string
// HTML内容
HtmlContent string
Must *actions.Must Must *actions.Must
CSRF *actionutils.CSRF CSRF *actionutils.CSRF
}) { }) {
@@ -119,6 +122,13 @@ func (this *UpdatePopupAction) RunPost(params struct {
actionParams = &firewallconfigs.FirewallActionHTTPAPIConfig{ actionParams = &firewallconfigs.FirewallActionHTTPAPIConfig{
URL: params.HttpAPIURL, URL: params.HttpAPIURL,
} }
case firewallconfigs.FirewallActionTypeHTML:
params.Must.
Field("htmlContent", params.HtmlContent).
Require("请输入HTML内容")
actionParams = &firewallconfigs.FirewallActionHTMLConfig{
Content: params.HtmlContent,
}
default: default:
this.Fail("选择的类型'" + params.Type + "'暂时不支持") this.Fail("选择的类型'" + params.Type + "'暂时不支持")
} }

View File

@@ -1,4 +1,4 @@
package settings package health
import ( import (
"encoding/json" "encoding/json"
@@ -9,16 +9,16 @@ import (
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
) )
type HealthAction struct { type IndexAction struct {
actionutils.ParentAction actionutils.ParentAction
} }
func (this *HealthAction) Init() { func (this *IndexAction) Init() {
this.Nav("", "setting", "") this.Nav("", "setting", "")
this.SecondMenu("health") this.SecondMenu("health")
} }
func (this *HealthAction) RunGet(params struct { func (this *IndexAction) RunGet(params struct {
ClusterId int64 ClusterId int64
}) { }) {
configResp, err := this.RPC().NodeClusterRPC().FindNodeClusterHealthCheckConfig(this.AdminContext(), &pb.FindNodeClusterHealthCheckConfigRequest{NodeClusterId: params.ClusterId}) configResp, err := this.RPC().NodeClusterRPC().FindNodeClusterHealthCheckConfig(this.AdminContext(), &pb.FindNodeClusterHealthCheckConfigRequest{NodeClusterId: params.ClusterId})
@@ -40,7 +40,7 @@ func (this *HealthAction) RunGet(params struct {
this.Show() this.Show()
} }
func (this *HealthAction) RunPost(params struct { func (this *IndexAction) RunPost(params struct {
ClusterId int64 ClusterId int64
HealthCheckJSON []byte HealthCheckJSON []byte
Must *actions.Must Must *actions.Must

View File

@@ -1,4 +1,4 @@
package settings package health
import ( import (
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs" "github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
@@ -7,20 +7,19 @@ import (
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
) )
type HealthRunPopupAction struct { type RunPopupAction struct {
actionutils.ParentAction actionutils.ParentAction
} }
func (this *HealthRunPopupAction) Init() { func (this *RunPopupAction) Init() {
this.Nav("", "", "") this.Nav("", "", "")
} }
func (this *HealthRunPopupAction) RunGet(params struct{}) { func (this *RunPopupAction) RunGet(params struct{}) {
this.Show() this.Show()
} }
func (this *HealthRunPopupAction) RunPost(params struct { func (this *RunPopupAction) RunPost(params struct {
ClusterId int64 ClusterId int64
Must *actions.Must Must *actions.Must

View File

@@ -5,6 +5,7 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/cache" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/cache"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/dns" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/dns"
firewallActions "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/firewall-actions" firewallActions "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/firewall-actions"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/health"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/services" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/services"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/toa" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/toa"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/waf" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/waf"
@@ -22,8 +23,8 @@ func init() {
GetPost("", new(IndexAction)). GetPost("", new(IndexAction)).
// 健康检查 // 健康检查
GetPost("/health", new(HealthAction)). GetPost("/health", new(health.IndexAction)).
GetPost("/healthRunPopup", new(HealthRunPopupAction)). GetPost("/health/runPopup", new(health.RunPopupAction)).
// 缓存 // 缓存
GetPost("/cache", new(cache.IndexAction)). GetPost("/cache", new(cache.IndexAction)).

View File

@@ -1,11 +1,13 @@
package clusterutils package clusterutils
import ( import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc" "github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/utils/numberutils" "github.com/TeaOSLab/EdgeAdmin/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/logs" "github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
@@ -86,16 +88,27 @@ func (this *ClusterHelper) createSettingMenu(cluster *pb.NodeCluster, selectedIt
"isActive": selectedItem == "waf", "isActive": selectedItem == "waf",
"isOn": cluster.HttpFirewallPolicyId > 0, "isOn": cluster.HttpFirewallPolicyId > 0,
}) })
items = append(items, maps.Map{
"name": "WAF动作", {
"url": "/clusters/cluster/settings/firewall-actions?clusterId=" + clusterId, hasActions, _ := this.checkFirewallActions(cluster.Id)
"isActive": selectedItem == "firewallAction", items = append(items, maps.Map{
}) "name": "WAF动作",
items = append(items, maps.Map{ "url": "/clusters/cluster/settings/firewall-actions?clusterId=" + clusterId,
"name": "健康检查", "isActive": selectedItem == "firewallAction",
"url": "/clusters/cluster/settings/health?clusterId=" + clusterId, "isOn": hasActions,
"isActive": selectedItem == "health", })
}) }
{
healthCheckIsOn, _ := this.checkHealthCheckIsOn(cluster.Id)
items = append(items, maps.Map{
"name": "健康检查",
"url": "/clusters/cluster/settings/health?clusterId=" + clusterId,
"isActive": selectedItem == "health",
"isOn": healthCheckIsOn,
})
}
items = append(items, maps.Map{ items = append(items, maps.Map{
"name": "DNS设置", "name": "DNS设置",
"url": "/clusters/cluster/settings/dns?clusterId=" + clusterId, "url": "/clusters/cluster/settings/dns?clusterId=" + clusterId,
@@ -114,3 +127,37 @@ func (this *ClusterHelper) createSettingMenu(cluster *pb.NodeCluster, selectedIt
}) })
return return
} }
// 检查健康检查是否开启
func (this *ClusterHelper) checkHealthCheckIsOn(clusterId int64) (bool, error) {
rpcClient, err := rpc.SharedRPC()
if err != nil {
return false, err
}
resp, err := rpcClient.NodeClusterRPC().FindNodeClusterHealthCheckConfig(rpcClient.Context(0), &pb.FindNodeClusterHealthCheckConfigRequest{NodeClusterId: clusterId})
if err != nil {
return false, err
}
if len(resp.HealthCheckJSON) > 0 {
healthCheckConfig := &serverconfigs.HealthCheckConfig{}
err = json.Unmarshal(resp.HealthCheckJSON, healthCheckConfig)
if err != nil {
return false, err
}
return healthCheckConfig.IsOn, nil
}
return false, nil
}
// 检查是否有WAF动作
func (this *ClusterHelper) checkFirewallActions(clusterId int64) (bool, error) {
rpcClient, err := rpc.SharedRPC()
if err != nil {
return false, err
}
resp, err := rpcClient.NodeClusterFirewallActionRPC().CountAllEnabledNodeClusterFirewallActions(rpcClient.Context(0), &pb.CountAllEnabledNodeClusterFirewallActionsRequest{NodeClusterId: clusterId})
if err != nil {
return false, err
}
return resp.Count > 0, nil
}

View File

@@ -65,6 +65,7 @@ func (this *IndexAction) RunGet(params struct {
} else { } else {
this.Data["version"] = teaconst.Version this.Data["version"] = teaconst.Version
} }
this.Data["faviconFileId"] = config.FaviconFileId
this.Show() this.Show()
} }

View File

@@ -6,6 +6,7 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
) )
@@ -28,7 +29,8 @@ func (this *CreatePopupAction) RunPost(params struct {
Type string Type string
// file // file
FileDir string FileDir string
FileMemoryCapacityJSON []byte
CapacityJSON []byte CapacityJSON []byte
MaxSizeJSON []byte MaxSizeJSON []byte
@@ -50,8 +52,21 @@ func (this *CreatePopupAction) RunPost(params struct {
params.Must. params.Must.
Field("fileDir", params.FileDir). Field("fileDir", params.FileDir).
Require("请输入缓存目录") Require("请输入缓存目录")
memoryCapacity := &shared.SizeCapacity{}
if len(params.FileMemoryCapacityJSON) > 0 {
err := json.Unmarshal(params.FileMemoryCapacityJSON, memoryCapacity)
if err != nil {
this.ErrorPage(err)
return
}
}
options = &serverconfigs.HTTPFileCacheStorage{ options = &serverconfigs.HTTPFileCacheStorage{
Dir: params.FileDir, Dir: params.FileDir,
MemoryPolicy: &serverconfigs.HTTPCachePolicy{
Capacity: memoryCapacity,
},
} }
case serverconfigs.CachePolicyStorageMemory: case serverconfigs.CachePolicyStorageMemory:
options = &serverconfigs.HTTPMemoryCacheStorage{ options = &serverconfigs.HTTPMemoryCacheStorage{

View File

@@ -6,6 +6,7 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
) )
@@ -52,7 +53,8 @@ func (this *UpdateAction) RunPost(params struct {
Type string Type string
// file // file
FileDir string FileDir string
FileMemoryCapacityJSON []byte
CapacityJSON []byte CapacityJSON []byte
MaxSizeJSON []byte MaxSizeJSON []byte
@@ -74,8 +76,21 @@ func (this *UpdateAction) RunPost(params struct {
params.Must. params.Must.
Field("fileDir", params.FileDir). Field("fileDir", params.FileDir).
Require("请输入缓存目录") Require("请输入缓存目录")
memoryCapacity := &shared.SizeCapacity{}
if len(params.FileMemoryCapacityJSON) > 0 {
err := json.Unmarshal(params.FileMemoryCapacityJSON, memoryCapacity)
if err != nil {
this.ErrorPage(err)
return
}
}
options = &serverconfigs.HTTPFileCacheStorage{ options = &serverconfigs.HTTPFileCacheStorage{
Dir: params.FileDir, Dir: params.FileDir,
MemoryPolicy: &serverconfigs.HTTPCachePolicy{
Capacity: memoryCapacity,
},
} }
case serverconfigs.CachePolicyStorageMemory: case serverconfigs.CachePolicyStorageMemory:
options = &serverconfigs.HTTPMemoryCacheStorage{ options = &serverconfigs.HTTPMemoryCacheStorage{
@@ -91,14 +106,14 @@ func (this *UpdateAction) RunPost(params struct {
} }
_, err = this.RPC().HTTPCachePolicyRPC().UpdateHTTPCachePolicy(this.AdminContext(), &pb.UpdateHTTPCachePolicyRequest{ _, err = this.RPC().HTTPCachePolicyRPC().UpdateHTTPCachePolicy(this.AdminContext(), &pb.UpdateHTTPCachePolicyRequest{
HttpCachePolicyId: params.CachePolicyId, HttpCachePolicyId: params.CachePolicyId,
IsOn: params.IsOn, IsOn: params.IsOn,
Name: params.Name, Name: params.Name,
Description: params.Description, Description: params.Description,
CapacityJSON: params.CapacityJSON, CapacityJSON: params.CapacityJSON,
MaxKeys: params.MaxKeys, MaxKeys: params.MaxKeys,
MaxSizeJSON: params.MaxSizeJSON, MaxSizeJSON: params.MaxSizeJSON,
Type: params.Type, Type: params.Type,
OptionsJSON: optionsJSON, OptionsJSON: optionsJSON,
}) })
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)

View File

@@ -10,6 +10,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
"strings"
) )
type CreateAction struct { type CreateAction struct {
@@ -215,6 +216,22 @@ func (this *CreateAction) RunPost(params struct {
if err != nil { if err != nil {
this.Fail("域名解析失败:" + err.Error()) this.Fail("域名解析失败:" + err.Error())
} }
// 检查域名是否已经存在
allServerNames := serverconfigs.PlainServerNames(serverNames)
if len(allServerNames) > 0 {
dupResp, err := this.RPC().ServerRPC().CheckServerNameDuplicationInNodeCluster(this.AdminContext(), &pb.CheckServerNameDuplicationInNodeClusterRequest{
ServerNames: allServerNames,
NodeClusterId: params.ClusterId,
})
if err != nil {
this.ErrorPage(err)
return
}
if len(dupResp.DuplicatedServerNames) > 0 {
this.Fail("域名 " + strings.Join(dupResp.DuplicatedServerNames, ", ") + " 已经被其他服务所占用,不能重复使用")
}
}
} }
// 源站地址 // 源站地址

View File

@@ -9,6 +9,7 @@ import (
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps" "github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time" timeutil "github.com/iwind/TeaGo/utils/time"
"strings"
) )
// 域名管理 // 域名管理
@@ -74,6 +75,34 @@ func (this *IndexAction) RunPost(params struct {
this.Fail("域名解析失败:" + err.Error()) this.Fail("域名解析失败:" + err.Error())
} }
serverResp, err := this.RPC().ServerRPC().FindEnabledUserServerBasic(this.AdminContext(), &pb.FindEnabledUserServerBasicRequest{ServerId: params.ServerId})
if err != nil {
this.ErrorPage(err)
return
}
if serverResp.Server == nil || serverResp.Server.NodeCluster == nil {
this.NotFound("server", params.ServerId)
return
}
clusterId := serverResp.Server.NodeCluster.Id
// 检查域名是否已经存在
allServerNames := serverconfigs.PlainServerNames(serverNames)
if len(allServerNames) > 0 {
dupResp, err := this.RPC().ServerRPC().CheckServerNameDuplicationInNodeCluster(this.AdminContext(), &pb.CheckServerNameDuplicationInNodeClusterRequest{
ServerNames: allServerNames,
NodeClusterId: clusterId,
ExcludeServerId: params.ServerId,
})
if err != nil {
this.ErrorPage(err)
return
}
if len(dupResp.DuplicatedServerNames) > 0 {
this.Fail("域名 " + strings.Join(dupResp.DuplicatedServerNames, ", ") + " 已经被其他服务所占用,不能重复使用")
}
}
_, err = this.RPC().ServerRPC().UpdateServerNames(this.AdminContext(), &pb.UpdateServerNamesRequest{ _, err = this.RPC().ServerRPC().UpdateServerNames(this.AdminContext(), &pb.UpdateServerNamesRequest{
ServerId: params.ServerId, ServerId: params.ServerId,
ServerNamesJSON: []byte(params.ServerNames), ServerNamesJSON: []byte(params.ServerNames),

View File

@@ -75,7 +75,7 @@ func (this *ClientsAction) RunGet(params struct {
"count": stat.Count, "count": stat.Count,
"system": maps.Map{ "system": maps.Map{
"id": stat.ClientSystem.Id, "id": stat.ClientSystem.Id,
"name": stat.ClientSystem.Name, "name": stat.ClientSystem.Name + " " + stat.Version,
}, },
}) })
} }
@@ -97,7 +97,7 @@ func (this *ClientsAction) RunGet(params struct {
"count": stat.Count, "count": stat.Count,
"browser": maps.Map{ "browser": maps.Map{
"id": stat.ClientBrowser.Id, "id": stat.ClientBrowser.Id,
"name": stat.ClientBrowser.Name, "name": stat.ClientBrowser.Name + " " + stat.Version,
}, },
}) })
} }

View File

@@ -91,6 +91,7 @@ func (this *UploadPopupAction) RunPost(params struct {
_, err = this.RPC().FileRPC().UpdateFileFinished(this.AdminContext(), &pb.UpdateFileFinishedRequest{FileId: fileId}) _, err = this.RPC().FileRPC().UpdateFileFinished(this.AdminContext(), &pb.UpdateFileFinishedRequest{FileId: fileId})
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)
return
} }
// 保存 // 保存

View File

@@ -3,7 +3,9 @@ package ui
import ( import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders" "github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
"io"
) )
type IndexAction struct { type IndexAction struct {
@@ -32,10 +34,14 @@ func (this *IndexAction) RunPost(params struct {
ShowFinance bool ShowFinance bool
ShowVersion bool ShowVersion bool
Version string Version string
FaviconFile *actions.File
LogoFile *actions.File
Must *actions.Must Must *actions.Must
CSRF *actionutils.CSRF CSRF *actionutils.CSRF
}) { }) {
defer this.CreateLogInfo("修改管理界面设置")
params.Must. params.Must.
Field("productName", params.ProductName). Field("productName", params.ProductName).
Require("请输入产品名称"). Require("请输入产品名称").
@@ -53,6 +59,101 @@ func (this *IndexAction) RunPost(params struct {
config.ShowFinance = params.ShowFinance config.ShowFinance = params.ShowFinance
config.ShowVersion = params.ShowVersion config.ShowVersion = params.ShowVersion
config.Version = params.Version config.Version = params.Version
// 上传Favicon文件
if params.FaviconFile != nil {
createResp, err := this.RPC().FileRPC().CreateFile(this.AdminContext(), &pb.CreateFileRequest{
Filename: params.FaviconFile.Filename,
Size: params.FaviconFile.Size,
IsPublic: true,
})
if err != nil {
this.ErrorPage(err)
return
}
fileId := createResp.FileId
// 上传内容
buf := make([]byte, 512*1024)
reader, err := params.FaviconFile.OriginFile.Open()
if err != nil {
this.ErrorPage(err)
return
}
for {
n, err := reader.Read(buf)
if n > 0 {
_, err = this.RPC().FileChunkRPC().CreateFileChunk(this.AdminContext(), &pb.CreateFileChunkRequest{
FileId: fileId,
Data: buf[:n],
})
if err != nil {
this.Fail("上传失败:" + err.Error())
}
}
if err != nil {
if err == io.EOF {
break
}
this.Fail("上传失败:" + err.Error())
}
}
// 置为已完成
_, err = this.RPC().FileRPC().UpdateFileFinished(this.AdminContext(), &pb.UpdateFileFinishedRequest{FileId: fileId})
if err != nil {
this.ErrorPage(err)
}
config.FaviconFileId = fileId
}
// 上传Logo文件
if params.LogoFile != nil {
createResp, err := this.RPC().FileRPC().CreateFile(this.AdminContext(), &pb.CreateFileRequest{
Filename: params.LogoFile.Filename,
Size: params.LogoFile.Size,
IsPublic: true,
})
if err != nil {
this.ErrorPage(err)
return
}
fileId := createResp.FileId
// 上传内容
buf := make([]byte, 512*1024)
reader, err := params.LogoFile.OriginFile.Open()
if err != nil {
this.ErrorPage(err)
return
}
for {
n, err := reader.Read(buf)
if n > 0 {
_, err = this.RPC().FileChunkRPC().CreateFileChunk(this.AdminContext(), &pb.CreateFileChunkRequest{
FileId: fileId,
Data: buf[:n],
})
if err != nil {
this.Fail("上传失败:" + err.Error())
}
}
if err != nil {
if err == io.EOF {
break
}
this.Fail("上传失败:" + err.Error())
}
}
// 置为已完成
_, err = this.RPC().FileRPC().UpdateFileFinished(this.AdminContext(), &pb.UpdateFileFinishedRequest{FileId: fileId})
if err != nil {
this.ErrorPage(err)
}
config.LogoFileId = fileId
}
err = configloaders.UpdateAdminUIConfig(config) err = configloaders.UpdateAdminUIConfig(config)
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)

View File

@@ -3,7 +3,9 @@ package userui
import ( import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders" "github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/actions"
"io"
) )
type IndexAction struct { type IndexAction struct {
@@ -32,6 +34,8 @@ func (this *IndexAction) RunPost(params struct {
ShowVersion bool ShowVersion bool
Version string Version string
ShowFinance bool ShowFinance bool
FaviconFile *actions.File
LogoFile *actions.File
Must *actions.Must Must *actions.Must
CSRF *actionutils.CSRF CSRF *actionutils.CSRF
@@ -53,6 +57,101 @@ func (this *IndexAction) RunPost(params struct {
config.ShowVersion = params.ShowVersion config.ShowVersion = params.ShowVersion
config.Version = params.Version config.Version = params.Version
config.ShowFinance = params.ShowFinance config.ShowFinance = params.ShowFinance
// 上传Favicon文件
if params.FaviconFile != nil {
createResp, err := this.RPC().FileRPC().CreateFile(this.AdminContext(), &pb.CreateFileRequest{
Filename: params.FaviconFile.Filename,
Size: params.FaviconFile.Size,
IsPublic: true,
})
if err != nil {
this.ErrorPage(err)
return
}
fileId := createResp.FileId
// 上传内容
buf := make([]byte, 512*1024)
reader, err := params.FaviconFile.OriginFile.Open()
if err != nil {
this.ErrorPage(err)
return
}
for {
n, err := reader.Read(buf)
if n > 0 {
_, err = this.RPC().FileChunkRPC().CreateFileChunk(this.AdminContext(), &pb.CreateFileChunkRequest{
FileId: fileId,
Data: buf[:n],
})
if err != nil {
this.Fail("上传失败:" + err.Error())
}
}
if err != nil {
if err == io.EOF {
break
}
this.Fail("上传失败:" + err.Error())
}
}
// 置为已完成
_, err = this.RPC().FileRPC().UpdateFileFinished(this.AdminContext(), &pb.UpdateFileFinishedRequest{FileId: fileId})
if err != nil {
this.ErrorPage(err)
}
config.FaviconFileId = fileId
}
// 上传Logo文件
if params.LogoFile != nil {
createResp, err := this.RPC().FileRPC().CreateFile(this.AdminContext(), &pb.CreateFileRequest{
Filename: params.LogoFile.Filename,
Size: params.LogoFile.Size,
IsPublic: true,
})
if err != nil {
this.ErrorPage(err)
return
}
fileId := createResp.FileId
// 上传内容
buf := make([]byte, 512*1024)
reader, err := params.LogoFile.OriginFile.Open()
if err != nil {
this.ErrorPage(err)
return
}
for {
n, err := reader.Read(buf)
if n > 0 {
_, err = this.RPC().FileChunkRPC().CreateFileChunk(this.AdminContext(), &pb.CreateFileChunkRequest{
FileId: fileId,
Data: buf[:n],
})
if err != nil {
this.Fail("上传失败:" + err.Error())
}
}
if err != nil {
if err == io.EOF {
break
}
this.Fail("上传失败:" + err.Error())
}
}
// 置为已完成
_, err = this.RPC().FileRPC().UpdateFileFinished(this.AdminContext(), &pb.UpdateFileFinishedRequest{FileId: fileId})
if err != nil {
this.ErrorPage(err)
}
config.LogoFileId = fileId
}
err = configloaders.UpdateUserUIConfig(config) err = configloaders.UpdateUserUIConfig(config)
if err != nil { if err != nil {
this.ErrorPage(err) this.ErrorPage(err)

View File

@@ -0,0 +1,68 @@
package ui
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"mime"
"path/filepath"
"strconv"
)
// 公开的图片,不需要检查用户权限
type ImageAction struct {
actionutils.ParentAction
}
func (this *ImageAction) Init() {
this.Nav("", "", "")
}
func (this *ImageAction) RunGet(params struct {
FileId int64
}) {
fileResp, err := this.RPC().FileRPC().FindEnabledFile(this.AdminContext(), &pb.FindEnabledFileRequest{FileId: params.FileId})
if err != nil {
this.ErrorPage(err)
return
}
file := fileResp.File
if file == nil {
this.NotFound("file", params.FileId)
return
}
if !file.IsPublic {
this.NotFound("file", params.FileId)
return
}
chunkIdsResp, err := this.RPC().FileChunkRPC().FindAllFileChunkIds(this.AdminContext(), &pb.FindAllFileChunkIdsRequest{FileId: file.Id})
if err != nil {
this.ErrorPage(err)
return
}
mimeType := ""
if len(file.Filename) > 0 {
ext := filepath.Ext(file.Filename)
mimeType = mime.TypeByExtension(ext)
}
if len(mimeType) == 0 {
mimeType = "image/png"
}
this.AddHeader("Last-Modified", "Fri, 06 Sep 2019 08:29:50 GMT")
this.AddHeader("Content-Type", mimeType)
this.AddHeader("Content-Length", strconv.FormatInt(file.Size, 10))
for _, chunkId := range chunkIdsResp.FileChunkIds {
chunkResp, err := this.RPC().FileChunkRPC().DownloadFileChunk(this.AdminContext(), &pb.DownloadFileChunkRequest{FileChunkId: chunkId})
if err != nil {
this.ErrorPage(err)
return
}
if chunkResp.FileChunk == nil {
continue
}
this.Write(chunkResp.FileChunk.Data)
}
}

View File

@@ -13,6 +13,9 @@ func init() {
server. server.
Prefix("/ui"). Prefix("/ui").
// 公共可以访问的链接
Get("/image/:fileId", new(ImageAction)).
// 以下的需要压缩 // 以下的需要压缩
Helper(&actions.Gzip{Level: gzip.BestCompression}). Helper(&actions.Gzip{Level: gzip.BestCompression}).
Get("/components.js", new(ComponentsAction)). Get("/components.js", new(ComponentsAction)).

View File

@@ -88,6 +88,8 @@ func (this *userMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam
action.Data["teaShowVersion"] = config.ShowVersion action.Data["teaShowVersion"] = config.ShowVersion
action.Data["teaTitle"] = config.AdminSystemName action.Data["teaTitle"] = config.AdminSystemName
action.Data["teaName"] = config.ProductName action.Data["teaName"] = config.ProductName
action.Data["teaFaviconFileId"] = config.FaviconFileId
action.Data["teaLogoFileId"] = config.LogoFileId
action.Data["teaUsername"] = configloaders.FindAdminFullname(adminId) action.Data["teaUsername"] = configloaders.FindAdminFullname(adminId)
action.Data["teaUserAvatar"] = "" action.Data["teaUserAvatar"] = ""

View File

@@ -1,131 +1,150 @@
Vue.component("health-check-config-box", { Vue.component("health-check-config-box", {
props: ["v-health-check-config"], props: ["v-health-check-config"],
data: function () { data: function () {
let healthCheckConfig = this.vHealthCheckConfig let healthCheckConfig = this.vHealthCheckConfig
let urlProtocol = "http" let urlProtocol = "http"
let urlPort = "" let urlPort = ""
let urlRequestURI = "/" let urlRequestURI = "/"
let urlHost = ""
if (healthCheckConfig == null) { if (healthCheckConfig == null) {
healthCheckConfig = { healthCheckConfig = {
isOn: false, isOn: false,
url: "", url: "",
interval: {count: 60, unit: "second"}, interval: {count: 60, unit: "second"},
statusCodes: [200], statusCodes: [200],
timeout: {count: 10, unit: "second"}, timeout: {count: 10, unit: "second"},
countTries: 3, countTries: 3,
tryDelay: {count: 100, unit: "ms"}, tryDelay: {count: 100, unit: "ms"},
autoDown: true, autoDown: true,
countUp: 1, countUp: 1,
countDown: 1 countDown: 1
} }
let that = this let that = this
setTimeout(function () { setTimeout(function () {
that.changeURL() that.changeURL()
}, 500) }, 500)
} else { } else {
try { try {
let url = new URL(healthCheckConfig.url) let url = new URL(healthCheckConfig.url)
urlProtocol = url.protocol.substring(0, url.protocol.length - 1) urlProtocol = url.protocol.substring(0, url.protocol.length - 1)
urlPort = url.port
urlRequestURI = url.pathname
if (url.search.length > 0) {
urlRequestURI += url.search
}
} catch (e) {
}
if (healthCheckConfig.statusCodes == null) { // 域名
healthCheckConfig.statusCodes = [200] urlHost = url.host
} if (urlHost == "%24%7Bhost%7D") {
if (healthCheckConfig.interval == null) { urlHost = "${host}"
healthCheckConfig.interval = {count: 60, unit: "second"} }
} let colonIndex = urlHost.indexOf(":")
if (healthCheckConfig.timeout == null) { if (colonIndex > 0) {
healthCheckConfig.timeout = {count: 10, unit: "second"} urlHost = urlHost.substring(0, colonIndex)
} }
if (healthCheckConfig.tryDelay == null) {
healthCheckConfig.tryDelay = {count: 100, unit: "ms"} urlPort = url.port
} urlRequestURI = url.pathname
if (healthCheckConfig.countUp == null || healthCheckConfig.countUp < 1) { if (url.search.length > 0) {
healthCheckConfig.countUp = 1 urlRequestURI += url.search
} }
if (healthCheckConfig.countDown == null || healthCheckConfig.countDown < 1) { } catch (e) {
healthCheckConfig.countDown = 1 }
}
} if (healthCheckConfig.statusCodes == null) {
console.log(healthCheckConfig.countUp, healthCheckConfig.countDown) healthCheckConfig.statusCodes = [200]
return { }
healthCheck: healthCheckConfig, if (healthCheckConfig.interval == null) {
advancedVisible: false, healthCheckConfig.interval = {count: 60, unit: "second"}
urlProtocol: urlProtocol, }
urlPort: urlPort, if (healthCheckConfig.timeout == null) {
urlRequestURI: urlRequestURI healthCheckConfig.timeout = {count: 10, unit: "second"}
} }
}, if (healthCheckConfig.tryDelay == null) {
watch: { healthCheckConfig.tryDelay = {count: 100, unit: "ms"}
urlRequestURI: function () { }
if (this.urlRequestURI.length > 0 && this.urlRequestURI[0] != "/") { if (healthCheckConfig.countUp == null || healthCheckConfig.countUp < 1) {
this.urlRequestURI = "/" + this.urlRequestURI healthCheckConfig.countUp = 1
} }
this.changeURL() if (healthCheckConfig.countDown == null || healthCheckConfig.countDown < 1) {
}, healthCheckConfig.countDown = 1
urlPort: function (v) { }
let port = parseInt(v) }
if (!isNaN(port)) { return {
this.urlPort = port.toString() healthCheck: healthCheckConfig,
} else { advancedVisible: false,
this.urlPort = "" urlProtocol: urlProtocol,
} urlHost: urlHost,
this.changeURL() urlPort: urlPort,
}, urlRequestURI: urlRequestURI
urlProtocol: function () { }
this.changeURL() },
}, watch: {
"healthCheck.countTries": function (v) { urlRequestURI: function () {
let count = parseInt(v) if (this.urlRequestURI.length > 0 && this.urlRequestURI[0] != "/") {
if (!isNaN(count)) { this.urlRequestURI = "/" + this.urlRequestURI
this.healthCheck.countTries = count }
} else { this.changeURL()
this.healthCheck.countTries = 0 },
} urlPort: function (v) {
}, let port = parseInt(v)
"healthCheck.countUp": function (v) { if (!isNaN(port)) {
let count = parseInt(v) this.urlPort = port.toString()
if (!isNaN(count)) { } else {
this.healthCheck.countUp = count this.urlPort = ""
} else { }
this.healthCheck.countUp = 0 this.changeURL()
} },
}, urlProtocol: function () {
"healthCheck.countDown": function (v) { this.changeURL()
let count = parseInt(v) },
if (!isNaN(count)) { urlHost: function () {
this.healthCheck.countDown = count this.changeURL()
} else { },
this.healthCheck.countDown = 0 "healthCheck.countTries": function (v) {
} let count = parseInt(v)
} if (!isNaN(count)) {
}, this.healthCheck.countTries = count
methods: { } else {
showAdvanced: function () { this.healthCheck.countTries = 0
this.advancedVisible = !this.advancedVisible }
}, },
changeURL: function () { "healthCheck.countUp": function (v) {
this.healthCheck.url = this.urlProtocol + "://${host}" + ((this.urlPort.length > 0) ? ":" + this.urlPort : "") + this.urlRequestURI let count = parseInt(v)
}, if (!isNaN(count)) {
changeStatus: function (values) { this.healthCheck.countUp = count
this.healthCheck.statusCodes = values.$map(function (k, v) { } else {
let status = parseInt(v) this.healthCheck.countUp = 0
if (isNaN(status)) { }
return 0 },
} else { "healthCheck.countDown": function (v) {
return status let count = parseInt(v)
} if (!isNaN(count)) {
}) this.healthCheck.countDown = count
} } else {
}, this.healthCheck.countDown = 0
template: `<div> }
}
},
methods: {
showAdvanced: function () {
this.advancedVisible = !this.advancedVisible
},
changeURL: function () {
let urlHost = this.urlHost
if (urlHost.length == 0) {
urlHost = "${host}"
}
this.healthCheck.url = this.urlProtocol + "://" + urlHost + ((this.urlPort.length > 0) ? ":" + this.urlPort : "") + this.urlRequestURI
},
changeStatus: function (values) {
this.healthCheck.statusCodes = values.$map(function (k, v) {
let status = parseInt(v)
if (isNaN(status)) {
return 0
} else {
return status
}
})
}
},
template: `<div>
<input type="hidden" name="healthCheckJSON" :value="JSON.stringify(healthCheck)"/> <input type="hidden" name="healthCheckJSON" :value="JSON.stringify(healthCheck)"/>
<table class="ui table definition selectable"> <table class="ui table definition selectable">
<tbody> <tbody>
@@ -143,28 +162,40 @@ Vue.component("health-check-config-box", {
<tr> <tr>
<td>URL *</td> <td>URL *</td>
<td> <td>
<div class="ui fields inline"> <table class="ui table">
<div class="ui field"> <tr>
<select class="ui dropdown" v-model="urlProtocol"> <td class="title">协议</td>
<td>
<select class="ui dropdown auto-width" v-model="urlProtocol">
<option value="http">http://</option> <option value="http">http://</option>
<option value="https">https://</option> <option value="https">https://</option>
</select> </select>
</div> </td>
<div class="ui field"> </tr>
<var style="color:grey">\${host}</var> <tr>
</div> <td>域名</td>
<div class="ui field">:</div> <td>
<div class="ui field"> <input type="text" v-model="urlHost"/>
<input type="text" maxlength="5" style="width:5.4em" placeholder="端口" v-model="urlPort"/> </td>
</div> </tr>
<div class="ui field"> <tr>
<input type="text" v-model="urlRequestURI" placeholder="/" style="width:23em"/> <td>端口</td>
</div> <td>
</div> <input type="text" maxlength="5" style="width:5.4em" placeholder="端口" v-model="urlPort"/>
</td>
</tr>
<tr>
<td>RequestURI</td>
<td><input type="text" v-model="urlRequestURI" placeholder="/" style="width:20em"/></td>
</tr>
</table>
<div class="ui divider"></div> <div class="ui divider"></div>
<p class="comment" v-if="healthCheck.url.length > 0">拼接后的URL<code-label>{{healthCheck.url}}</code-label>,其中\${host}指的是节点地址。</p> <p class="comment" v-if="healthCheck.url.length > 0">拼接后的URL<code-label>{{healthCheck.url}}</code-label>,其中\${host}指的是域名。</p>
</td> </td>
</tr> </tr>
<tr>
<td></td>
</tr>
<tr> <tr>
<td>检测时间间隔</td> <td>检测时间间隔</td>
<td> <td>

View File

@@ -132,15 +132,21 @@ Vue.component("http-access-log-config-box", {
<p class="comment">选中表示只输出日志到日志策略,而停止默认的日志存储。</p> <p class="comment">选中表示只输出日志到日志策略,而停止默认的日志存储。</p>
</td> </td>
</tr> </tr>
<tr>
<td>是否只记录WAF相关日志</td>
<td>
<checkbox v-model="accessLog.firewallOnly"></checkbox>
<p class="comment">选中后只记录WAF相关的日志。通过此选项可有效减少访问日志数量降低网络带宽和存储压力。</p>
</td>
</tr>
</tbody> </tbody>
</table> </table>
<div v-show="(!vIsLocation || accessLog.isPrior) && accessLog.isOn">
<h4>WAF相关</h4>
<table class="ui table definition selectable">
<tr>
<td class="title">是否只记录WAF相关日志</td>
<td>
<checkbox v-model="accessLog.firewallOnly"></checkbox>
<p class="comment">选中后只记录WAF相关的日志。通过此选项可有效减少访问日志数量降低网络带宽和存储压力。</p>
</td>
</tr>
</table>
</div>
<div class="margin"></div> <div class="margin"></div>
</div>` </div>`
}) })

View File

@@ -20,6 +20,8 @@ Vue.component("reverse-proxy-box", {
requestHostType: 0, requestHostType: 0,
addHeaders: [] addHeaders: []
} }
} else if (reverseProxyConfig.addHeaders == null) {
reverseProxyConfig.addHeaders = []
} }
let forwardHeaders = [ let forwardHeaders = [

View File

@@ -4,7 +4,11 @@
<title>{$.teaTitle}</title> <title>{$.teaTitle}</title>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
{$if eq .teaFaviconFileId 0}
<link rel="shortcut icon" href="/images/favicon.png"/> <link rel="shortcut icon" href="/images/favicon.png"/>
{$else}
<link rel="shortcut icon" href="/ui/image/{$.teaFaviconFileId}"/>
{$end}
<link rel="stylesheet" type="text/css" href="/_/@default/@layout.css" media="all"/> <link rel="stylesheet" type="text/css" href="/_/@default/@layout.css" media="all"/>
{$TEA.SEMANTIC} {$TEA.SEMANTIC}
<link rel="stylesheet" type="text/css" href="/_/@default/@layout_override.css" media="all"/> <link rel="stylesheet" type="text/css" href="/_/@default/@layout_override.css" media="all"/>
@@ -23,7 +27,7 @@
<!-- 顶部导航 --> <!-- 顶部导航 -->
<div class="ui menu top-nav blue inverted small borderless" v-cloak=""> <div class="ui menu top-nav blue inverted small borderless" v-cloak="">
<a href="/" class="item"> <a href="/" class="item">
<i class="ui icon leaf"></i> &nbsp; {{teaTitle}}&nbsp;<sup v-if="teaShowVersion">v{{teaVersion}}</sup> &nbsp; <i class="ui icon leaf" v-if="teaLogoFileId == 0"></i><img v-if="teaLogoFileId > 0" :src="'/ui/image/' + teaLogoFileId"/> &nbsp; {{teaTitle}}&nbsp;<sup v-if="teaShowVersion">v{{teaVersion}}</sup> &nbsp;
</a> </a>
<div class="right menu"> <div class="right menu">

View File

@@ -1,14 +1,16 @@
Tea.context(function () { Tea.context(function () {
this.deleteCluster = function (clusterId) { this.deleteCluster = function (clusterId) {
let that = this let that = this
teaweb.confirm("确定要删除此集群吗?", function () { teaweb.confirm("确定要删除此集群吗?", function () {
that.$post("/clusters/cluster/delete") that.$post("/clusters/cluster/delete")
.params({ .params({
clusterId: clusterId clusterId: clusterId
}) })
.success(function () { .success(function () {
window.location = "/clusters" teaweb.success("删除成功", function () {
}) window.location = "/clusters"
}) })
} })
})
}
}) })

View File

@@ -22,7 +22,7 @@
<td style="text-align: center;"><i class="icon bars handle grey"></i> </td> <td style="text-align: center;"><i class="icon bars handle grey"></i> </td>
<td>{{group.name}}</td> <td>{{group.name}}</td>
<td class="center"> <td class="center">
<span v-if="group.countNodes.length > 0">{{group.countNodes}}</span> <span v-if="group.countNodes > 0">{{group.countNodes}}</span>
<span v-else class="disabled">0</span> <span v-else class="disabled">0</span>
</td> </td>
<td> <td>

View File

@@ -97,7 +97,10 @@
</td> </td>
<td class="center"> <td class="center">
<div v-if="!node.isUp"> <div v-if="!node.isUp">
<span class="red">健康问题</span> <span class="red">健康问题下线</span>
<div>
<a href="" @click.prevent="upNode(node.id)">[上线]</a>
</div>
</div> </div>
<div v-else-if="!node.isOn"> <div v-else-if="!node.isOn">
<label-on :v-is-on="node.isOn"></label-on> <label-on :v-is-on="node.isOn"></label-on>

View File

@@ -1,11 +1,21 @@
Tea.context(function () { Tea.context(function () {
this.deleteNode = function (nodeId) { this.deleteNode = function (nodeId) {
teaweb.confirm("确定要删除这个节点吗?", function () { teaweb.confirm("确定要删除这个节点吗?", function () {
this.$post("/nodes/delete") this.$post("/nodes/delete")
.params({ .params({
nodeId: nodeId nodeId: nodeId
}) })
.refresh(); .refresh();
}); })
}; }
});
this.upNode = function (nodeId) {
teaweb.confirm("确定要手动上线此节点吗?", function () {
this.$post("/clusters/cluster/node/up")
.params({
nodeId: nodeId
})
.refresh()
})
}
})

View File

@@ -92,6 +92,25 @@
</td> </td>
</tr> </tr>
</tbody> </tbody>
<!-- HTML -->
<tbody v-if="type == 'html'">
<tr>
<td>HTML内容 *</td>
<td>
<textarea name="htmlContent" maxlength="128000" placeholder="完整的HTML内容">&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;zh&#34;&gt;
&lt;head&gt;
&lt;title&gt;&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;!-- 这里写提示文字 --&gt;
&lt;/body&gt;
&lt;/html&gt;</textarea>
</td>
</tr>
</tbody>
</table> </table>
<submit-btn></submit-btn> <submit-btn></submit-btn>
</form> </form>

View File

@@ -92,6 +92,16 @@
</td> </td>
</tr> </tr>
</tbody> </tbody>
<!-- HTML -->
<tbody v-if="type == 'html'">
<tr>
<td>HTML内容 *</td>
<td>
<textarea name="htmlContent" maxlength="128000" placeholder="完整的HTML内容" v-model="action.params.content"></textarea>
</td>
</tr>
</tbody>
</table> </table>
<submit-btn></submit-btn> <submit-btn></submit-btn>
</form> </form>

View File

@@ -3,7 +3,7 @@ Tea.context(function () {
this.run = function () { this.run = function () {
teaweb.confirm("确定要对当前集群下的所有节点进行健康检查吗?", function () { teaweb.confirm("确定要对当前集群下的所有节点进行健康检查吗?", function () {
teaweb.popup("/clusters/cluster/settings/healthRunPopup?clusterId=" + this.clusterId, { teaweb.popup("/clusters/cluster/settings/health/runPopup?clusterId=" + this.clusterId, {
height: "25em" height: "25em"
}) })
}) })

View File

@@ -2,7 +2,11 @@
<html lang="zh"> <html lang="zh">
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
{$if eq .faviconFileId 0}
<link rel="shortcut icon" href="/images/favicon.png"/> <link rel="shortcut icon" href="/images/favicon.png"/>
{$else}
<link rel="shortcut icon" href="/ui/image/{$ .faviconFileId}"/>
{$end}
<title>登录{$.systemName}</title> <title>登录{$.systemName}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
{$TEA.VUE} {$TEA.VUE}

View File

@@ -25,16 +25,32 @@
<p class="comment">存放文件缓存的目录,通常填写绝对路径。</p> <p class="comment">存放文件缓存的目录,通常填写绝对路径。</p>
</td> </td>
</tr> </tr>
<tr>
<td class="color-border">文件目录最大容量</td>
<td>
<size-capacity-box :v-name="'capacityJSON'" :v-count="0" :v-unit="'gb'"></size-capacity-box>
<p class="comment">作为二级缓存的文件目录允许缓存的最大容量如果为0表示没有限制。</p>
</td>
</tr>
<tr>
<td class="color-border">内存最大容量</td>
<td>
<size-capacity-box :v-name="'fileMemoryCapacityJSON'" :v-count="1" :v-unit="'gb'"></size-capacity-box>
<p class="comment">作为一级缓存的内存最大容量如果为0表示不使用内存作为一级缓存。</p>
</td>
</tr>
</tbody> </tbody>
<tbody v-if="policyType != 'file'">
<tr>
<td>内存最大容量</td>
<td>
<size-capacity-box :v-name="'capacityJSON'" :v-count="0" :v-unit="'gb'"></size-capacity-box>
<p class="comment">允许缓存的最大容量如果为0表示没有限制。</p>
</td>
</tr>
</tbody>
<tr>
<td>缓存最大容量</td>
<td>
<size-capacity-box :v-name="'capacityJSON'" :v-count="0" :v-unit="'gb'"></size-capacity-box>
<p class="comment">允许缓存的最大内容长度如果为0表示没有限制。</p>
</td>
</tr>
<tr> <tr>
<td colspan="2"><more-options-indicator></more-options-indicator></td> <td colspan="2"><more-options-indicator></more-options-indicator></td>
</tr> </tr>

View File

@@ -19,49 +19,64 @@
<!-- 文件缓存选项 --> <!-- 文件缓存选项 -->
<tbody v-if="cachePolicy.type == 'file'"> <tbody v-if="cachePolicy.type == 'file'">
<tr> <tr>
<td class="color-border">缓存目录</td> <td class="color-border">缓存目录</td>
<td> <td>
{{cachePolicy.options.dir}} {{cachePolicy.options.dir}}
<p class="comment">存放文件缓存的目录,通常填写绝对路径。</p> <p class="comment">存放文件缓存的目录,通常填写绝对路径。</p>
</td> </td>
</tr> </tr>
<tr>
<td class="color-border">文件目录最大容量</td>
<td>
<size-capacity-view :v-value="cachePolicy.capacity" :v-default-text="'不限'"></size-capacity-view>
<p class="comment">作为二级缓存的文件目录允许缓存的最大容量如果为0表示没有限制。</p>
</td>
</tr>
<tr v-if="cachePolicy.options.memoryPolicy != null && cachePolicy.options.memoryPolicy.capacity != null && cachePolicy.options.memoryPolicy.capacity.count > 0">
<td class="color-border">内存最大容量</td>
<td>
<size-capacity-view :v-value="cachePolicy.options.memoryPolicy.capacity"></size-capacity-view>
<p class="comment">作为一级缓存的内存最大容量如果为0表示不使用内存作为一级缓存。</p>
</td>
</tr>
</tbody> </tbody>
<tbody v-if="cachePolicy.type != 'file'">
<tr> <tr>
<td>缓存最大容量</td> <td>缓存最大容量</td>
<td> <td>
<size-capacity-view :v-value="cachePolicy.capacity" :v-default-text="'不限'"></size-capacity-view> <size-capacity-view :v-value="cachePolicy.capacity" :v-default-text="'不限'"></size-capacity-view>
<p class="comment">允许缓存的最大内容长度如果为0表示没有限制。</p> <p class="comment">允许缓存的最大容量如果为0表示没有限制。</p>
</td> </td>
</tr> </tr>
</tbody>
<tr> <tr>
<td colspan="2"><more-options-indicator></more-options-indicator></td> <td colspan="2"><more-options-indicator></more-options-indicator></td>
</tr> </tr>
<tbody v-show="moreOptionsVisible"> <tbody v-show="moreOptionsVisible">
<tr> <tr>
<td>最大内容长度</td> <td>最大内容长度</td>
<td> <td>
<size-capacity-view :v-value="cachePolicy.maxSize" :v-default-text="'不限'"></size-capacity-view> <size-capacity-view :v-value="cachePolicy.maxSize" :v-default-text="'不限'"></size-capacity-view>
<p class="comment">允许缓存的最大内容长度如果为0表示没有限制。</p> <p class="comment">允许缓存的最大内容长度如果为0表示没有限制。</p>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>容纳Key数量</td> <td>容纳Key数量</td>
<td> <td>
<span v-if="cachePolicy.maxKeys > 0">{{cachePolicy.maxKeys}}</span> <span v-if="cachePolicy.maxKeys > 0">{{cachePolicy.maxKeys}}</span>
<span v-else>不限</span> <span v-else>不限</span>
<p class="comment">可以容纳多少数量的Key0表示不限制。</p> <p class="comment">可以容纳多少数量的Key0表示不限制。</p>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>描述</td> <td>描述</td>
<td> <td>
<span v-if="cachePolicy.description.length > 0">{{cachePolicy.description}}</span> <span v-if="cachePolicy.description.length > 0">{{cachePolicy.description}}</span>
<span v-else class="disabled">暂时还没有描述。</span> <span v-else class="disabled">暂时还没有描述。</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -27,16 +27,38 @@
<p class="comment">存放文件缓存的目录,通常填写绝对路径。</p> <p class="comment">存放文件缓存的目录,通常填写绝对路径。</p>
</td> </td>
</tr> </tr>
<tr>
<td class="color-border">文件目录最大容量</td>
<td>
<size-capacity-box :v-name="'capacityJSON'" :v-value="cachePolicy.capacity" :v-count="0" :v-unit="'gb'"></size-capacity-box>
<p class="comment">作为二级缓存的文件目录允许缓存的最大容量如果为0表示没有限制。</p>
</td>
</tr>
<tr v-if="cachePolicy.options.memoryPolicy != null && cachePolicy.options.memoryPolicy.capacity != null">
<td class="color-border">内存最大容量</td>
<td>
<size-capacity-box :v-name="'fileMemoryCapacityJSON'" :v-value="cachePolicy.options.memoryPolicy.capacity" :v-count="1" :v-unit="'gb'"></size-capacity-box>
<p class="comment">作为一级缓存的内存最大容量如果为0表示不使用内存作为一级缓存。</p>
</td>
</tr>
<tr v-if="cachePolicy.options.memoryPolicy == null || cachePolicy.options.memoryPolicy.capacity == null">
<td class="color-border">内存最大容量</td>
<td>
<size-capacity-box :v-name="'fileMemoryCapacityJSON'" :v-count="0" :v-unit="'gb'"></size-capacity-box>
<p class="comment">作为一级缓存的内存最大容量如果为0表示不使用内存作为一级缓存。</p>
</td>
</tr>
</tbody> </tbody>
<tbody v-if="policyType != 'file'">
<tr> <tr>
<td>缓存最大容量</td> <td>缓存最大容量</td>
<td> <td>
<size-capacity-box :v-name="'capacityJSON'" :v-value="cachePolicy.capacity" :v-count="0" :v-unit="'gb'"></size-capacity-box> <size-capacity-box :v-name="'capacityJSON'" :v-value="cachePolicy.capacity" :v-count="0" :v-unit="'gb'"></size-capacity-box>
<p class="comment">允许缓存的最大内容长度如果为0表示没有限制。</p> <p class="comment">允许缓存的最大容量如果为0表示没有限制。</p>
</td> </td>
</tr> </tr>
</tbody>
<tr> <tr>
<td colspan="2"><more-options-indicator></more-options-indicator></td> <td colspan="2"><more-options-indicator></more-options-indicator></td>
</tr> </tr>

View File

@@ -33,7 +33,10 @@ Tea.context(function () {
let chart = echarts.init(chartBox) let chart = echarts.init(chartBox)
let option = { let option = {
xAxis: { xAxis: {
data: stats.map(xFunc) data: stats.map(xFunc),
axisLabel: {
interval: 0
}
}, },
yAxis: { yAxis: {
axisLabel: { axisLabel: {

View File

@@ -38,7 +38,10 @@ Tea.context(function () {
let chart = echarts.init(chartBox) let chart = echarts.init(chartBox)
let option = { let option = {
xAxis: { xAxis: {
data: stats.map(xFunc) data: stats.map(xFunc),
axisLabel: {
interval: 0
}
}, },
yAxis: { yAxis: {
axisLabel: { axisLabel: {

View File

@@ -21,7 +21,10 @@ Tea.context(function () {
let chart = echarts.init(chartBox) let chart = echarts.init(chartBox)
let option = { let option = {
xAxis: { xAxis: {
data: stats.map(xFunc) data: stats.map(xFunc),
axisLabel: {
interval: 0
}
}, },
yAxis: { yAxis: {
axisLabel: { axisLabel: {

View File

@@ -41,6 +41,36 @@
<checkbox name="showFinance" v-model="config.showFinance"></checkbox> <checkbox name="showFinance" v-model="config.showFinance"></checkbox>
</td> </td>
</tr> </tr>
<tr>
<td>浏览器图标</td>
<td>
<div v-if="config.faviconFileId > 0">
<a :href="'/ui/image/' + config.faviconFileId" target="_blank"><img :src="'/ui/image/' + config.faviconFileId" style="width:32px;border:1px #ccc solid;"/></a>
</div>
<div v-else>
<span class="disabled">还没有上传。</span>
</div>
<div style="margin-top: 0.8em">
<input type="file" name="faviconFile" accept=".png"/>
</div>
<p class="comment">在浏览器标签栏显示的图标请使用PNG格式。</p>
</td>
</tr>
<tr>
<td>Logo</td>
<td>
<div v-if="config.logoFileId > 0">
<a :href="'/ui/image/' + config.logoFileId" target="_blank"><img :src="'/ui/image/' + config.logoFileId" style="width:32px;border:1px #ccc solid;"/></a>
</div>
<div v-else>
<span class="disabled">还没有上传。</span>
</div>
<div style="margin-top: 0.8em">
<input type="file" name="logoFile" accept=".png"/>
</div>
<p class="comment">显示在系统界面上的图标请使用PNG格式。</p>
</td>
</tr>
</table> </table>
<submit-btn></submit-btn> <submit-btn></submit-btn>

View File

@@ -41,6 +41,36 @@
<checkbox name="showFinance" v-model="config.showFinance"></checkbox> <checkbox name="showFinance" v-model="config.showFinance"></checkbox>
</td> </td>
</tr> </tr>
<tr>
<td>浏览器图标</td>
<td>
<div v-if="config.faviconFileId > 0">
<a :href="'/ui/image/' + config.faviconFileId" target="_blank"><img :src="'/ui/image/' + config.faviconFileId" style="width:32px;border:1px #ccc solid;"/></a>
</div>
<div v-else>
<span class="disabled">还没有上传。</span>
</div>
<div style="margin-top: 0.8em">
<input type="file" name="faviconFile" accept=".png"/>
</div>
<p class="comment">在浏览器标签栏显示的图标请使用PNG格式。</p>
</td>
</tr>
<tr>
<td>Logo</td>
<td>
<div v-if="config.logoFileId > 0">
<a :href="'/ui/image/' + config.logoFileId" target="_blank"><img :src="'/ui/image/' + config.logoFileId" style="width:32px;border:1px #ccc solid;"/></a>
</div>
<div v-else>
<span class="disabled">还没有上传。</span>
</div>
<div style="margin-top: 0.8em">
<input type="file" name="logoFile" accept=".png"/>
</div>
<p class="comment">显示在系统界面上的图标请使用PNG格式。</p>
</td>
</tr>
</table> </table>
<p class="comment">修改后,可能需要等待数分钟才会生效。</p> <p class="comment">修改后,可能需要等待数分钟才会生效。</p>