Compare commits

..

53 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
刘祥超
245c4374b4 增加IP级别和WAF动作 2021-02-06 17:37:09 +08:00
刘祥超
ed302370e9 优化系统消息交互 2021-02-03 11:18:41 +08:00
刘祥超
b2feb452e0 可以批量删除同步任务 2021-02-02 20:52:46 +08:00
刘祥超
8b7661d82d 在WAF中增加检查IP状态功能 2021-02-02 19:30:07 +08:00
刘祥超
33c9cd0819 单个服务里也增加国家/地区、省份封禁 2021-02-02 16:23:23 +08:00
刘祥超
a0635a97d5 IP名单新增IPv6和所有IP两种类型 2021-02-02 15:25:11 +08:00
刘祥超
e22a9d061c 修复$HOME未定义问题 2021-02-01 09:25:57 +08:00
刘祥超
1b9d62c7bd 优化界面 2021-02-01 09:25:31 +08:00
刘祥超
2ca31ffcb4 界面显示时增加默认线路值 2021-01-31 16:09:55 +08:00
刘祥超
14636ed82f 可以批量远程安装和升级节点 2021-01-31 16:03:52 +08:00
刘祥超
cc5a34c20e 节点详情中显示更多信息(版本、CPU、负载等) 2021-01-31 11:05:04 +08:00
刘祥超
3ebb47d915 消息提示可以单个标为已读 2021-01-29 10:39:19 +08:00
刘祥超
3eea58c1fc 优化JS加载速度/修改蓝色背景 2021-01-29 10:04:00 +08:00
刘祥超
255bf58abf 优化JS代码加载速度 2021-01-28 17:04:36 +08:00
刘祥超
b5b324bca0 DNS更新任务增加域名更新 2021-01-28 15:39:10 +08:00
刘祥超
74e654446e 优化界面 2021-01-28 11:35:22 +08:00
刘祥超
be01536a09 支持自定义HTTP DNS 2021-01-28 11:29:50 +08:00
刘祥超
4c6bcc7c19 增加DNS同步任务状态显示 2021-01-27 22:59:46 +08:00
刘祥超
98dceb20db 优化界面 2021-01-27 11:53:42 +08:00
刘祥超
25833bc81a 增加管理界面截图 2021-01-27 11:40:32 +08:00
刘祥超
0d5fc7ad85 systemd Service安装失败时直接返回 2021-01-26 21:32:03 +08:00
刘祥超
b9af3d4757 修改README,加入组件源码地址等 2021-01-26 21:31:32 +08:00
刘祥超
f179b0a60e 优化界面 2021-01-26 20:35:29 +08:00
刘祥超
875162cfb6 可以配置是否在反向代理中添加X-Real-IP和X-Forwarded-* 2021-01-26 20:29:29 +08:00
刘祥超
2f21effdbf 实现WAF统计 2021-01-26 18:41:23 +08:00
刘祥超
f7336235a1 缩短右上菜单中同步状态检测时间 2021-01-26 10:29:53 +08:00
刘祥超
6ed0791991 访问日志可以设置只记录WAF相关日志 2021-01-26 10:29:29 +08:00
刘祥超
045a18fb22 删除不必要的文件 2021-01-25 18:40:38 +08:00
刘祥超
dbe598a934 对服务增加基础的数据统计 2021-01-25 16:40:49 +08:00
刘祥超
2812b30b01 优化界面 2021-01-24 14:41:43 +08:00
刘祥超
386d4957e7 修正一处文字提示错误(白名单->黑名单) 2021-01-21 20:46:08 +08:00
刘祥超
ea36e60899 优化Dashboard界面 2021-01-21 19:22:06 +08:00
刘祥超
bbf7e2898f 实现Dashboard 2021-01-21 18:55:53 +08:00
刘祥超
beab50de4c 变更版本号 2021-01-21 11:32:43 +08:00
刘祥超
a3ac7678d9 补充README文档 2021-01-21 11:32:32 +08:00
201 changed files with 6226 additions and 1373 deletions

View File

@@ -1 +1,26 @@
![架构](doc/architect-zh.jpg)
# GoEdge目标
做一款人人用得起的CDN & WAF系统。
![架构](doc/screenshot.png)
## 特性
* `免费` - 开源、免费、自由、开放
* `简单` - 架构简单清晰,安装简单,使用简单,运维简单
* `高扩展性` - 可以自由扩展新的节点,支持亿级数据
## 文档
[点这里查看文档](https://github.com/TeaOSLab/EdgeDocs)
## 架构
![架构](doc/architect-zh.jpg)
其中的组件源码地址如下:
* [边缘节点](https://github.com/TeaOSLab/EdgeNode)
* [API节点](https://github.com/TeaOSLab/EdgeAPI)
* [管理平台](https://github.com/TeaOSLab/EdgeAdmin)
## 联系我们
有什么问题和建议都可以加入QQ群 `659832182`
## 感谢
* 感谢[JetBrains公司](https://www.jetbrains.com/)提供免费的IDE开发Licence。

View File

@@ -1,4 +1,4 @@
api.yaml
server.yaml
api_db.yaml
api-123.yaml
*.pem

View File

@@ -1,6 +0,0 @@
rpc:
endpoints:
- http://192.168.2.40:8003
nodeId: H6sjDf779jimnVPnBFSgZxvr6Ca0wQ0z
secret: hMHjmEng0SIcT3yiA3HIoUjogwAC9cur

View File

@@ -22,6 +22,7 @@ func main() {
err := nodes.NewAdminNode().InstallSystemService()
if err != nil {
fmt.Println("[ERROR]install failed: " + err.Error())
return
}
fmt.Println("done")
})

BIN
doc/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

View File

@@ -10,15 +10,16 @@ import (
type AdminModuleCode = string
const (
AdminModuleCodeServer AdminModuleCode = "server" // 网站
AdminModuleCodeNode AdminModuleCode = "node" // 节点
AdminModuleCodeDNS AdminModuleCode = "dns" // DNS
AdminModuleCodeAdmin AdminModuleCode = "admin" // 系统用户
AdminModuleCodeUser AdminModuleCode = "user" // 平台用户
AdminModuleCodeFinance AdminModuleCode = "finance" // 财务
AdminModuleCodeLog AdminModuleCode = "log" // 日志
AdminModuleCodeSetting AdminModuleCode = "setting" // 设置
AdminModuleCodeCommon AdminModuleCode = "common" // 只要登录就可以访问的模块
AdminModuleCodeDashboard AdminModuleCode = "dashboard" // 看板
AdminModuleCodeServer AdminModuleCode = "server" // 网站
AdminModuleCodeNode AdminModuleCode = "node" // 节点
AdminModuleCodeDNS AdminModuleCode = "dns" // DNS
AdminModuleCodeAdmin AdminModuleCode = "admin" // 系统用户
AdminModuleCodeUser AdminModuleCode = "user" // 平台用户
AdminModuleCodeFinance AdminModuleCode = "finance" // 财务
AdminModuleCodeLog AdminModuleCode = "log" // 日志
AdminModuleCodeSetting AdminModuleCode = "setting" // 设置
AdminModuleCodeCommon AdminModuleCode = "common" // 只要登录就可以访问的模块
)
var sharedAdminModuleMapping = map[int64]*AdminModuleList{} // adminId => AdminModuleList
@@ -109,7 +110,7 @@ func FindFirstAdminModule(adminId int64) (module AdminModuleCode, ok bool) {
list, ok2 := sharedAdminModuleMapping[adminId]
if ok2 {
if list.IsSuper {
return AdminModuleCodeServer, true
return AdminModuleCodeDashboard, true
} else if len(list.Modules) > 0 {
return list.Modules[0].Code, true
}
@@ -132,6 +133,11 @@ func FindAdminFullname(adminId int64) string {
// 所有权限列表
func AllModuleMaps() []maps.Map {
return []maps.Map{
{
"name": "看板",
"code": AdminModuleCodeDashboard,
"url": "/dashboard",
},
{
"name": "网站服务",
"code": AdminModuleCodeServer,

View File

@@ -24,13 +24,14 @@ func LoadAPIConfig() (*APIConfig, error) {
localFile := Tea.ConfigFile("api.yaml")
isFromLocal := false
paths := []string{localFile}
homeDir, err := os.UserHomeDir()
if err == nil {
homeDir, homeErr := os.UserHomeDir()
if homeErr == nil {
paths = append(paths, homeDir+"/."+teaconst.ProcessName+"/api.yaml")
}
paths = append(paths, "/etc/"+teaconst.ProcessName+"/api.yaml")
var data []byte
var err error
for _, path := range paths {
data, err = ioutil.ReadFile(path)
if err == nil {
@@ -69,8 +70,8 @@ func (this *APIConfig) WriteFile(path string) error {
// 写入 ~/ 和 /etc/ 目录,因为是备份需要,所以不需要提示错误信息
// 写入 ~/.edge-admin/
filename := filepath.Base(path)
homeDir, err := os.UserHomeDir()
if err == nil {
homeDir, homeErr := os.UserHomeDir()
if homeErr == nil {
dir := homeDir + "/." + teaconst.ProcessName
stat, err := os.Stat(dir)
if err == nil && stat.IsDir() {

View File

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

View File

@@ -71,6 +71,10 @@ func (this *RPCClient) NodeClusterRPC() pb.NodeClusterServiceClient {
return pb.NewNodeClusterServiceClient(this.pickConn())
}
func (this *RPCClient) NodeClusterFirewallActionRPC() pb.NodeClusterFirewallActionServiceClient {
return pb.NewNodeClusterFirewallActionServiceClient(this.pickConn())
}
func (this *RPCClient) NodeGroupRPC() pb.NodeGroupServiceClient {
return pb.NewNodeGroupServiceClient(this.pickConn())
}
@@ -91,6 +95,34 @@ func (this *RPCClient) ServerRPC() pb.ServerServiceClient {
return pb.NewServerServiceClient(this.pickConn())
}
func (this *RPCClient) ServerClientSystemMonthlyStatRPC() pb.ServerClientSystemMonthlyStatServiceClient {
return pb.NewServerClientSystemMonthlyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerClientBrowserMonthlyStatRPC() pb.ServerClientBrowserMonthlyStatServiceClient {
return pb.NewServerClientBrowserMonthlyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerRegionCountryMonthlyStatRPC() pb.ServerRegionCountryMonthlyStatServiceClient {
return pb.NewServerRegionCountryMonthlyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerRegionProvinceMonthlyStatRPC() pb.ServerRegionProvinceMonthlyStatServiceClient {
return pb.NewServerRegionProvinceMonthlyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerRegionCityMonthlyStatRPC() pb.ServerRegionCityMonthlyStatServiceClient {
return pb.NewServerRegionCityMonthlyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerRegionProviderMonthlyStatRPC() pb.ServerRegionProviderMonthlyStatServiceClient {
return pb.NewServerRegionProviderMonthlyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerHTTPFirewallDailyStatRPC() pb.ServerHTTPFirewallDailyStatServiceClient {
return pb.NewServerHTTPFirewallDailyStatServiceClient(this.pickConn())
}
func (this *RPCClient) ServerGroupRPC() pb.ServerGroupServiceClient {
return pb.NewServerGroupServiceClient(this.pickConn())
}
@@ -236,6 +268,10 @@ func (this *RPCClient) DNSRPC() pb.DNSServiceClient {
return pb.NewDNSServiceClient(this.pickConn())
}
func (this *RPCClient) DNSTaskRPC() pb.DNSTaskServiceClient {
return pb.NewDNSTaskServiceClient(this.pickConn())
}
func (this *RPCClient) ACMEUserRPC() pb.ACMEUserServiceClient {
return pb.NewACMEUserServiceClient(this.pickConn())
}
@@ -306,6 +342,12 @@ func (this *RPCClient) APIContext(apiNodeId int64) context.Context {
return ctx
}
// 修改配置
func (this *RPCClient) UpdateConfig(config *configs.APIConfig) error {
this.apiConfig = config
return this.init()
}
// 初始化
func (this *RPCClient) init() error {
// 重新连接
@@ -333,6 +375,8 @@ func (this *RPCClient) init() error {
if len(conns) == 0 {
return errors.New("[RPC]no available endpoints")
}
// 这里不需要加锁因为会和pickConn冲突
this.conns = conns
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

@@ -2,18 +2,42 @@ package utils
import (
"encoding/binary"
"math/big"
"net"
"regexp"
"strings"
)
// 将IP转换为整型
func IP2Long(ip string) uint32 {
func IP2Long(ip string) uint64 {
s := net.ParseIP(ip)
if s == nil {
if len(s) != 16 {
return 0
}
if len(s) == 16 {
return binary.BigEndian.Uint32(s[12:16])
if strings.Contains(ip, ":") { // IPv6
bigInt := big.NewInt(0)
bigInt.SetBytes(s.To16())
return bigInt.Uint64()
}
return binary.BigEndian.Uint32(s)
return uint64(binary.BigEndian.Uint32(s.To4()))
}
// 判断是否为IPv4
func IsIPv4(ip string) bool {
if !regexp.MustCompile(`^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$`).MatchString(ip) {
return false
}
if IP2Long(ip) == 0 {
return false
}
return true
}
// 判断是否为IPv6
func IsIPv6(ip string) bool {
if !strings.Contains(ip, ":") {
return false
}
return len(net.ParseIP(ip)) == net.IPv6len
}

View File

@@ -0,0 +1,85 @@
package utils
import (
"testing"
)
func TestIP2Long(t *testing.T) {
for _, ip := range []string{
"0.0.0.1",
"0.0.0.2",
"127.0.0.1",
"192.0.0.2",
"255.255.255.255",
"2001:db8:0:1::101",
"2001:db8:0:1::102",
"2406:8c00:0:3409:133:18:203:0",
"2406:8c00:0:3409:133:18:203:158",
"2406:8c00:0:3409:133:18:203:159",
"2406:8c00:0:3409:133:18:203:160",
} {
t.Log(ip, " -> ", IP2Long(ip))
}
}
func TestIsIPv4(t *testing.T) {
type testIP struct {
ip string
ok bool
}
for _, item := range []testIP{
{
ip: "1",
ok: false,
},
{
ip: "192.168.0.1",
ok: true,
},
{
ip: "1.1.0.1",
ok: true,
},
{
ip: "255.255.255.255",
ok: true,
},
{
ip: "192.168.0.1233",
ok: false,
},
} {
if IsIPv4(item.ip) != item.ok {
t.Fatal(item.ip, "should be", item.ok)
}
}
}
func TestIsIPv6(t *testing.T) {
type testIP struct {
ip string
ok bool
}
for _, item := range []testIP{
{
ip: "1",
ok: false,
},
{
ip: "2406:8c00:0:3409:133:18:203:158",
ok: true,
},
{
ip: "2406::8c00:0:3409:133:18:203:158",
ok: false,
},
{
ip: "2001:db8:0:1::101",
ok: true,
},
} {
if IsIPv6(item.ip) != item.ok {
t.Fatal(item.ip, "should be", item.ok)
}
}
}

View File

@@ -17,12 +17,26 @@ func FormatBytes(bytes int64) string {
if bytes < 1024 {
return FormatInt64(bytes) + "B"
} else if bytes < 1024*1024 {
return fmt.Sprintf("%.2fK", float64(bytes)/1024)
return fmt.Sprintf("%.2fKB", float64(bytes)/1024)
} else if bytes < 1024*1024*1024 {
return fmt.Sprintf("%.2fM", float64(bytes)/1024/1024)
return fmt.Sprintf("%.2fMB", float64(bytes)/1024/1024)
} else if bytes < 1024*1024*1024*1024 {
return fmt.Sprintf("%.2fG", float64(bytes)/1024/1024/1024)
return fmt.Sprintf("%.2fGB", float64(bytes)/1024/1024/1024)
} else {
return fmt.Sprintf("%.2fP", float64(bytes)/1024/1024/1024/1024)
return fmt.Sprintf("%.2fPB", float64(bytes)/1024/1024/1024/1024)
}
}
func FormatBits(bits int64) string {
if bits < 1000 {
return FormatInt64(bits) + "B"
} else if bits < 1000*1000 {
return fmt.Sprintf("%.2fKB", float64(bits)/1000)
} else if bits < 1000*1000*1000 {
return fmt.Sprintf("%.2fMB", float64(bits)/1000/1000)
} else if bits < 1000*1000*1000*1000 {
return fmt.Sprintf("%.2fGB", float64(bits)/1000/1000/1000)
} else {
return fmt.Sprintf("%.2fPB", float64(bits)/1000/1000/1000/1000)
}
}

View File

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

View File

@@ -19,7 +19,7 @@ func (this *InstallManualAction) Init() {
func (this *InstallManualAction) RunGet(params struct {
ClusterId int64
}) {
this.Data["leftMenuItems"] = LeftMenuItemsForInstall(params.ClusterId, "manual")
this.Data["leftMenuItems"] = LeftMenuItemsForInstall(this.AdminContext(), params.ClusterId, "manual")
nodesResp, err := this.RPC().NodeRPC().FindAllNotInstalledNodesWithClusterId(this.AdminContext(), &pb.FindAllNotInstalledNodesWithClusterIdRequest{NodeClusterId: params.ClusterId})
if err != nil {

View File

@@ -19,7 +19,7 @@ func (this *InstallNodesAction) Init() {
func (this *InstallNodesAction) RunGet(params struct {
ClusterId int64
}) {
this.Data["leftMenuItems"] = LeftMenuItemsForInstall(params.ClusterId, "register")
this.Data["leftMenuItems"] = LeftMenuItemsForInstall(this.AdminContext(), params.ClusterId, "register")
clusterResp, err := this.RPC().NodeClusterRPC().FindEnabledNodeCluster(this.AdminContext(), &pb.FindEnabledNodeClusterRequest{NodeClusterId: params.ClusterId})
if err != nil {

View File

@@ -21,7 +21,7 @@ func (this *InstallRemoteAction) Init() {
func (this *InstallRemoteAction) RunGet(params struct {
ClusterId int64
}) {
this.Data["leftMenuItems"] = LeftMenuItemsForInstall(params.ClusterId, "install")
this.Data["leftMenuItems"] = LeftMenuItemsForInstall(this.AdminContext(), params.ClusterId, "install")
nodesResp, err := this.RPC().NodeRPC().FindAllNotInstalledNodesWithClusterId(this.AdminContext(), &pb.FindAllNotInstalledNodesWithClusterIdRequest{NodeClusterId: params.ClusterId})
if err != nil {

View File

@@ -89,6 +89,12 @@ func (this *NodeAction) RunGet(params struct {
})
}
}
if len(dnsRouteMaps) == 0 {
dnsRouteMaps = append(dnsRouteMaps, maps.Map{
"name": "",
"code": "",
})
}
this.Data["dnsRoutes"] = dnsRouteMaps
this.Data["dnsRecordName"] = recordName
this.Data["dnsRecordValue"] = recordValue
@@ -143,6 +149,24 @@ func (this *NodeAction) RunGet(params struct {
status.IsActive = status.IsActive && time.Now().Unix()-status.UpdatedAt <= 60 // N秒之内认为活跃
}
// 检查是否有新版本
if len(status.OS) > 0 {
checkVersionResp, err := this.RPC().NodeRPC().CheckNodeLatestVersion(this.AdminContext(), &pb.CheckNodeLatestVersionRequest{
Os: status.OS,
Arch: status.Arch,
CurrentVersion: status.BuildVersion,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["shouldUpgrade"] = checkVersionResp.HasNewVersion
this.Data["newVersion"] = checkVersionResp.NewVersion
} else {
this.Data["shouldUpgrade"] = false
this.Data["newVersion"] = ""
}
// 分组
var groupMap maps.Map = nil
if node.Group != nil {
@@ -175,14 +199,20 @@ func (this *NodeAction) RunGet(params struct {
"isOn": node.IsOn,
"status": maps.Map{
"isActive": status.IsActive,
"updatedAt": status.UpdatedAt,
"hostname": status.Hostname,
"cpuUsage": status.CPUUsage,
"cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100),
"memUsage": status.MemoryUsage,
"memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100),
"connectionCount": status.ConnectionCount,
"isActive": status.IsActive,
"updatedAt": status.UpdatedAt,
"hostname": status.Hostname,
"cpuUsage": status.CPUUsage,
"cpuUsageText": fmt.Sprintf("%.2f%%", status.CPUUsage*100),
"memUsage": status.MemoryUsage,
"memUsageText": fmt.Sprintf("%.2f%%", status.MemoryUsage*100),
"connectionCount": status.ConnectionCount,
"buildVersion": status.BuildVersion,
"cpuPhysicalCount": status.CPUPhysicalCount,
"cpuLogicalCount": status.CPULogicalCount,
"load1m": fmt.Sprintf("%.2f", status.Load1m),
"load5m": fmt.Sprintf("%.2f", status.Load5m),
"load15m": fmt.Sprintf("%.2f", status.Load15m),
},
"group": groupMap,

View File

@@ -1,8 +1,8 @@
package node
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
)
@@ -34,6 +34,7 @@ func (this *StatusAction) RunPost(params struct {
"isOk": node.InstallStatus.IsOk,
"updatedAt": node.InstallStatus.UpdatedAt,
"error": node.InstallStatus.Error,
"errorCode": node.InstallStatus.ErrorCode,
}
} else {
this.Data["installStatus"] = nil

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

@@ -0,0 +1,125 @@
package firewallActions
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
"github.com/iwind/TeaGo/actions"
)
type CreatePopupAction struct {
actionutils.ParentAction
}
func (this *CreatePopupAction) Init() {
this.Nav("", "", "")
}
func (this *CreatePopupAction) RunGet(params struct {
ClusterId int64
}) {
this.Data["clusterId"] = params.ClusterId
this.Data["actionTypes"] = firewallconfigs.FindAllFirewallActionTypes()
this.Show()
}
func (this *CreatePopupAction) RunPost(params struct {
ClusterId int64
Name string
EventLevel string
Type string
// ipset
IpsetWhiteName string
IpsetBlackName string
IpsetAutoAddToIPTables bool
IpsetAutoAddToFirewalld bool
// script
ScriptPath string
// http api
HttpAPIURL string
// html
HtmlContent string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
defer this.CreateLogInfo("创建WAF动作")
params.Must.
Field("name", params.Name).
Require("请输入动作名称").
Field("type", params.Type).
Require("请选择动作类型")
var actionParams interface{} = nil
switch params.Type {
case firewallconfigs.FirewallActionTypeIPSet:
params.Must.
Field("ipsetWhiteName", params.IpsetWhiteName).
Require("请输入IPSet白名单名称").
Match(`^\w+$`, "请输入正确的IPSet白名单名称").
Field("ipsetBlackName", params.IpsetBlackName).
Require("请输入IPSet黑名单名称").
Match(`^\w+$`, "请输入正确的IPSet黑名单名称")
actionParams = &firewallconfigs.FirewallActionIPSetConfig{
WhiteName: params.IpsetWhiteName,
BlackName: params.IpsetBlackName,
AutoAddToIPTables: params.IpsetAutoAddToIPTables,
AutoAddToFirewalld: params.IpsetAutoAddToFirewalld,
}
case firewallconfigs.FirewallActionTypeIPTables:
actionParams = &firewallconfigs.FirewallActionIPTablesConfig{}
case firewallconfigs.FirewallActionTypeFirewalld:
actionParams = &firewallconfigs.FirewallActionFirewalldConfig{}
case firewallconfigs.FirewallActionTypeScript:
params.Must.
Field("scriptPath", params.ScriptPath).
Require("请输入脚本路径")
actionParams = &firewallconfigs.FirewallActionScriptConfig{
Path: params.ScriptPath,
}
case firewallconfigs.FirewallActionTypeHTTPAPI:
params.Must.
Field("httpAPIURL", params.HttpAPIURL).
Require("请输入API URL").
Match(`^(http|https):`, "API地址必须以http://或https://开头")
actionParams = &firewallconfigs.FirewallActionHTTPAPIConfig{
URL: params.HttpAPIURL,
}
case firewallconfigs.FirewallActionTypeHTML:
params.Must.
Field("htmlContent", params.HtmlContent).
Require("请输入HTML内容")
actionParams = &firewallconfigs.FirewallActionHTMLConfig{
Content: params.HtmlContent,
}
default:
this.Fail("选择的类型'" + params.Type + "'暂时不支持")
}
actionParamsJSON, err := json.Marshal(actionParams)
if err != nil {
this.ErrorPage(err)
return
}
_, err = this.RPC().NodeClusterFirewallActionRPC().CreateNodeClusterFirewallAction(this.AdminContext(), &pb.CreateNodeClusterFirewallActionRequest{
NodeClusterId: params.ClusterId,
Name: params.Name,
EventLevel: params.EventLevel,
Type: params.Type,
ParamsJSON: actionParamsJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,24 @@
package firewallActions
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type DeleteAction struct {
actionutils.ParentAction
}
func (this *DeleteAction) RunPost(params struct {
ActionId int64
}) {
defer this.CreateLogInfo("删除WAF动作 %d", params.ActionId)
_, err := this.RPC().NodeClusterFirewallActionRPC().DeleteNodeClusterFirewallAction(this.AdminContext(), &pb.DeleteNodeClusterFirewallActionRequest{NodeClusterFirewallActionId: params.ActionId})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,65 @@
package firewallActions
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
"github.com/iwind/TeaGo/maps"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "setting", "")
this.SecondMenu("firewallAction")
}
func (this *IndexAction) RunGet(params struct {
ClusterId int64
}) {
actionsResp, err := this.RPC().NodeClusterFirewallActionRPC().FindAllEnabledNodeClusterFirewallActions(this.AdminContext(), &pb.FindAllEnabledNodeClusterFirewallActionsRequest{NodeClusterId: params.ClusterId})
if err != nil {
this.ErrorPage(err)
return
}
levelMaps := map[string][]maps.Map{} // level => actionMaps
for _, action := range actionsResp.NodeClusterFirewallActions {
actionMaps, ok := levelMaps[action.EventLevel]
if !ok {
actionMaps = []maps.Map{}
}
actionMaps = append(actionMaps, maps.Map{
"id": action.Id,
"name": action.Name,
"type": action.Type,
"typeName": firewallconfigs.FindFirewallActionTypeName(action.Type),
})
levelMaps[action.EventLevel] = actionMaps
}
levelMaps2 := []maps.Map{} // []levelMap
hasActions := false
for _, level := range firewallconfigs.FindAllFirewallEventLevels() {
actionMaps, ok := levelMaps[level.Code]
if !ok {
actionMaps = []maps.Map{}
} else {
hasActions = true
}
levelMaps2 = append(levelMaps2, maps.Map{
"name": level.Name,
"code": level.Code,
"actions": actionMaps,
})
}
this.Data["levels"] = levelMaps2
this.Data["hasActions"] = hasActions
this.Show()
}

View File

@@ -0,0 +1,154 @@
package firewallActions
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
type UpdatePopupAction struct {
actionutils.ParentAction
}
func (this *UpdatePopupAction) Init() {
this.Nav("", "", "")
}
func (this *UpdatePopupAction) RunGet(params struct {
ActionId int64
}) {
actionResp, err := this.RPC().NodeClusterFirewallActionRPC().FindEnabledNodeClusterFirewallAction(this.AdminContext(), &pb.FindEnabledNodeClusterFirewallActionRequest{NodeClusterFirewallActionId: params.ActionId})
if err != nil {
this.ErrorPage(err)
return
}
action := actionResp.NodeClusterFirewallAction
if action == nil {
this.NotFound("nodeClusterFirewallAction", params.ActionId)
return
}
actionParams := maps.Map{}
if len(action.ParamsJSON) > 0 {
err = json.Unmarshal(action.ParamsJSON, &actionParams)
if err != nil {
this.ErrorPage(err)
return
}
}
this.Data["action"] = maps.Map{
"id": action.Id,
"name": action.Name,
"eventLevel": action.EventLevel,
"params": actionParams,
"type": action.Type,
}
// 通用参数
this.Data["actionTypes"] = firewallconfigs.FindAllFirewallActionTypes()
this.Show()
}
func (this *UpdatePopupAction) RunPost(params struct {
ActionId int64
Name string
EventLevel string
Type string
// ipset
IpsetWhiteName string
IpsetBlackName string
IpsetAutoAddToIPTables bool
IpsetAutoAddToFirewalld bool
// script
ScriptPath string
// http api
HttpAPIURL string
// HTML内容
HtmlContent string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
defer this.CreateLogInfo("修改WAF动作 %d", params.ActionId)
params.Must.
Field("name", params.Name).
Require("请输入动作名称").
Field("type", params.Type).
Require("请选择动作类型")
var actionParams interface{} = nil
switch params.Type {
case firewallconfigs.FirewallActionTypeIPSet:
params.Must.
Field("ipsetWhiteName", params.IpsetWhiteName).
Require("请输入IPSet白名单名称").
Match(`^\w+$`, "请输入正确的IPSet白名单名称").
Field("ipsetBlackName", params.IpsetBlackName).
Require("请输入IPSet黑名单名称").
Match(`^\w+$`, "请输入正确的IPSet黑名单名称")
actionParams = &firewallconfigs.FirewallActionIPSetConfig{
WhiteName: params.IpsetWhiteName,
BlackName: params.IpsetBlackName,
AutoAddToIPTables: params.IpsetAutoAddToIPTables,
AutoAddToFirewalld: params.IpsetAutoAddToFirewalld,
}
case firewallconfigs.FirewallActionTypeIPTables:
actionParams = &firewallconfigs.FirewallActionIPTablesConfig{}
case firewallconfigs.FirewallActionTypeFirewalld:
actionParams = &firewallconfigs.FirewallActionFirewalldConfig{}
case firewallconfigs.FirewallActionTypeScript:
params.Must.
Field("scriptPath", params.ScriptPath).
Require("请输入脚本路径")
actionParams = &firewallconfigs.FirewallActionScriptConfig{
Path: params.ScriptPath,
}
case firewallconfigs.FirewallActionTypeHTTPAPI:
params.Must.
Field("httpAPIURL", params.HttpAPIURL).
Require("请输入API URL").
Match(`^(http|https):`, "API地址必须以http://或https://开头")
actionParams = &firewallconfigs.FirewallActionHTTPAPIConfig{
URL: params.HttpAPIURL,
}
case firewallconfigs.FirewallActionTypeHTML:
params.Must.
Field("htmlContent", params.HtmlContent).
Require("请输入HTML内容")
actionParams = &firewallconfigs.FirewallActionHTMLConfig{
Content: params.HtmlContent,
}
default:
this.Fail("选择的类型'" + params.Type + "'暂时不支持")
}
actionParamsJSON, err := json.Marshal(actionParams)
if err != nil {
this.ErrorPage(err)
return
}
_, err = this.RPC().NodeClusterFirewallActionRPC().UpdateNodeClusterFirewallAction(this.AdminContext(), &pb.UpdateNodeClusterFirewallActionRequest{
NodeClusterFirewallActionId: params.ActionId,
Name: params.Name,
EventLevel: params.EventLevel,
Type: params.Type,
ParamsJSON: actionParamsJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -1,4 +1,4 @@
package settings
package health
import (
"encoding/json"
@@ -9,16 +9,16 @@ import (
"github.com/iwind/TeaGo/actions"
)
type HealthAction struct {
type IndexAction struct {
actionutils.ParentAction
}
func (this *HealthAction) Init() {
func (this *IndexAction) Init() {
this.Nav("", "setting", "")
this.SecondMenu("health")
}
func (this *HealthAction) RunGet(params struct {
func (this *IndexAction) RunGet(params struct {
ClusterId int64
}) {
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()
}
func (this *HealthAction) RunPost(params struct {
func (this *IndexAction) RunPost(params struct {
ClusterId int64
HealthCheckJSON []byte
Must *actions.Must

View File

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

View File

@@ -4,6 +4,8 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/cache"
"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"
"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/toa"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings/waf"
@@ -21,8 +23,8 @@ func init() {
GetPost("", new(IndexAction)).
// 健康检查
GetPost("/health", new(HealthAction)).
GetPost("/healthRunPopup", new(HealthRunPopupAction)).
GetPost("/health", new(health.IndexAction)).
GetPost("/health/runPopup", new(health.RunPopupAction)).
// 缓存
GetPost("/cache", new(cache.IndexAction)).
@@ -43,6 +45,13 @@ func init() {
GetPost("", new(services.IndexAction)).
GetPost("/status", new(services.StatusAction)).
// 防火墙动作
Prefix("/clusters/cluster/settings/firewall-actions").
Get("", new(firewallActions.IndexAction)).
GetPost("/createPopup", new(firewallActions.CreatePopupAction)).
GetPost("/updatePopup", new(firewallActions.UpdatePopupAction)).
Post("/delete", new(firewallActions.DeleteAction)).
EndAll()
})
}

View File

@@ -21,7 +21,7 @@ func (this *UpgradeRemoteAction) Init() {
func (this *UpgradeRemoteAction) RunGet(params struct {
ClusterId int64
}) {
this.Data["leftMenuItems"] = LeftMenuItemsForInstall(params.ClusterId, "upgrade")
this.Data["leftMenuItems"] = LeftMenuItemsForInstall(this.AdminContext(), params.ClusterId, "upgrade")
nodes := []maps.Map{}
resp, err := this.RPC().NodeRPC().FindAllUpgradeNodesWithClusterId(this.AdminContext(), &pb.FindAllUpgradeNodesWithClusterIdRequest{NodeClusterId: params.ClusterId})

View File

@@ -1,12 +1,34 @@
package cluster
import (
"context"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
"strconv"
)
// 安装升级相关的左侧菜单
func LeftMenuItemsForInstall(clusterId int64, selectedItem string) []maps.Map {
func LeftMenuItemsForInstall(ctx context.Context, clusterId int64, selectedItem string) []maps.Map {
rpcClient, _ := rpc.SharedRPC()
countNotInstalled := int64(0)
countUpgrade := int64(0)
if rpcClient != nil {
{
resp, err := rpcClient.NodeRPC().CountAllNotInstalledNodesWithClusterId(ctx, &pb.CountAllNotInstalledNodesWithClusterIdRequest{NodeClusterId: clusterId})
if err == nil {
countNotInstalled = resp.Count
}
}
{
resp, err := rpcClient.NodeRPC().CountAllUpgradeNodesWithClusterId(ctx, &pb.CountAllUpgradeNodesWithClusterIdRequest{NodeClusterId: clusterId})
if err == nil {
countUpgrade = resp.Count
}
}
}
return []maps.Map{
{
"name": "手动安装",
@@ -19,12 +41,12 @@ func LeftMenuItemsForInstall(clusterId int64, selectedItem string) []maps.Map {
"isActive": selectedItem == "register",
},
{
"name": "远程安装",
"name": "远程安装(" + strconv.FormatInt(countNotInstalled, 10) + ")",
"url": "/clusters/cluster/installRemote?clusterId=" + numberutils.FormatInt64(clusterId),
"isActive": selectedItem == "install",
},
{
"name": "远程升级",
"name": "远程升级(" + strconv.FormatInt(countUpgrade, 10) + ")",
"url": "/clusters/cluster/upgradeRemote?clusterId=" + numberutils.FormatInt64(clusterId),
"isActive": selectedItem == "upgrade",
},

View File

@@ -1,11 +1,13 @@
package clusterutils
import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
@@ -86,15 +88,32 @@ func (this *ClusterHelper) createSettingMenu(cluster *pb.NodeCluster, selectedIt
"isActive": selectedItem == "waf",
"isOn": cluster.HttpFirewallPolicyId > 0,
})
items = append(items, maps.Map{
"name": "健康检查",
"url": "/clusters/cluster/settings/health?clusterId=" + clusterId,
"isActive": selectedItem == "health",
})
{
hasActions, _ := this.checkFirewallActions(cluster.Id)
items = append(items, maps.Map{
"name": "WAF动作",
"url": "/clusters/cluster/settings/firewall-actions?clusterId=" + clusterId,
"isActive": selectedItem == "firewallAction",
"isOn": hasActions,
})
}
{
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{
"name": "DNS设置",
"url": "/clusters/cluster/settings/dns?clusterId=" + clusterId,
"isActive": selectedItem == "dns",
"isOn": cluster.DnsDomainId > 0 || len(cluster.DnsName) > 0,
})
items = append(items, maps.Map{
"name": "系统服务",
@@ -108,3 +127,37 @@ func (this *ClusterHelper) createSettingMenu(cluster *pb.NodeCluster, selectedIt
})
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

@@ -0,0 +1,24 @@
package tasks
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type DeleteBatchAction struct {
actionutils.ParentAction
}
func (this *DeleteBatchAction) RunPost(params struct {
TaskIds []int64
}) {
defer this.CreateLogInfo("批量删除节点同步任务")
_, err := this.RPC().NodeTaskRPC().DeleteNodeTasks(this.AdminContext(), &pb.DeleteNodeTasksRequest{NodeTaskIds: params.TaskIds})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -16,6 +16,7 @@ func init() {
GetPost("/listPopup", new(ListPopupAction)).
Post("/check", new(CheckAction)).
Post("/delete", new(DeleteAction)).
Post("/deleteBatch", new(DeleteBatchAction)).
EndAll()
})

View File

@@ -2,7 +2,12 @@ package dashboard
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
"math"
"regexp"
)
type IndexAction struct {
@@ -17,13 +22,75 @@ func (this *IndexAction) RunGet(params struct{}) {
// 取得用户的权限
module, ok := configloaders.FindFirstAdminModule(this.AdminId())
if ok {
for _, m := range configloaders.AllModuleMaps() {
if m.GetString("code") == module {
this.RedirectURL(m.GetString("url"))
return
if module != "dashboard" {
for _, m := range configloaders.AllModuleMaps() {
if m.GetString("code") == module {
this.RedirectURL(m.GetString("url"))
return
}
}
}
}
// 读取看板数据
resp, err := this.RPC().AdminRPC().ComposeAdminDashboard(this.AdminContext(), &pb.ComposeAdminDashboardRequest{})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["dashboard"] = maps.Map{
"countServers": resp.CountServers,
"countNodeClusters": resp.CountNodeClusters,
"countNodes": resp.CountNodes,
"countUsers": resp.CountUsers,
"countAPINodes": resp.CountAPINodes,
"countDBNodes": resp.CountDBNodes,
"countUserNodes": resp.CountUserNodes,
"canGoServers": configloaders.AllowModule(this.AdminId(), configloaders.AdminModuleCodeServer),
"canGoNodes": configloaders.AllowModule(this.AdminId(), configloaders.AdminModuleCodeNode),
"canGoSettings": configloaders.AllowModule(this.AdminId(), configloaders.AdminModuleCodeSetting),
"canGoUsers": configloaders.AllowModule(this.AdminId(), configloaders.AdminModuleCodeUser),
}
// 今日流量
todayTrafficBytes := int64(0)
if len(resp.DailyTrafficStats) > 0 {
todayTrafficBytes = resp.DailyTrafficStats[len(resp.DailyTrafficStats)-1].Bytes
}
todayTrafficString := numberutils.FormatBits(todayTrafficBytes * 8)
result := regexp.MustCompile(`^(?U)(.+)([a-zA-Z]+)$`).FindStringSubmatch(todayTrafficString)
if len(result) > 2 {
this.Data["todayTraffic"] = result[1]
this.Data["todayTrafficUnit"] = result[2]
} else {
this.Data["todayTraffic"] = todayTrafficString
this.Data["todayTrafficUnit"] = ""
}
// 24小时流量趋势
{
statMaps := []maps.Map{}
for _, stat := range resp.HourlyTrafficStats {
statMaps = append(statMaps, maps.Map{
"count": math.Ceil((float64(stat.Bytes)*8/1000/1000/1000)*1000) / 1000,
"hour": stat.Hour[8:],
})
}
this.Data["hourlyTrafficStats"] = statMaps
}
// 15天流量趋势
{
statMaps := []maps.Map{}
for _, stat := range resp.DailyTrafficStats {
statMaps = append(statMaps, maps.Map{
"count": math.Ceil((float64(stat.Bytes)*8/1000/1000/1000)*1000) / 1000,
"day": stat.Day[4:6] + "月" + stat.Day[6:] + "日",
})
}
this.Data["dailyTrafficStats"] = statMaps
}
this.Show()
}

View File

@@ -9,6 +9,7 @@ import (
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.Prefix("/dashboard").
Data("teaMenu", "dashboard").
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeCommon)).
GetPost("", new(IndexAction)).
EndAll()

View File

@@ -6,6 +6,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/rands"
)
type CreatePopupAction struct {
@@ -32,6 +33,9 @@ func (this *CreatePopupAction) RunGet(params struct{}) {
}
this.Data["types"] = typeMaps
// 自动生成CustomHTTP私钥
this.Data["paramCustomHTTPSecret"] = rands.HexString(32)
this.Show()
}
@@ -51,6 +55,10 @@ func (this *CreatePopupAction) RunPost(params struct {
ParamApiKey string
ParamApiSecret string
// CustomHTTP
ParamCustomHTTPURL string
ParamCustomHTTPSecret string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
@@ -89,6 +97,15 @@ func (this *CreatePopupAction) RunPost(params struct {
apiParams["apiKey"] = params.ParamApiKey
apiParams["apiSecret"] = params.ParamApiSecret
case "customHTTP":
params.Must.
Field("paramCustomHTTPURL", params.ParamCustomHTTPURL).
Require("请输入HTTP URL").
Match("^(?i)(http|https):", "URL必须以http://或者https://开头").
Field("paramCustomHTTPSecret", params.ParamCustomHTTPSecret).
Require("请输入私钥")
apiParams["url"] = params.ParamCustomHTTPURL
apiParams["secret"] = params.ParamCustomHTTPSecret
default:
this.Fail("暂时不支持此服务商'" + params.Type + "'")
}

View File

@@ -84,6 +84,10 @@ func (this *UpdatePopupAction) RunPost(params struct {
ParamApiKey string
ParamApiSecret string
// CustomHTTP
ParamCustomHTTPURL string
ParamCustomHTTPSecret string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
@@ -124,6 +128,15 @@ func (this *UpdatePopupAction) RunPost(params struct {
apiParams["apiKey"] = params.ParamApiKey
apiParams["apiSecret"] = params.ParamApiSecret
case "customHTTP":
params.Must.
Field("paramCustomHTTPURL", params.ParamCustomHTTPURL).
Require("请输入HTTP URL").
Match("^(?i)(http|https):", "URL必须以http://或者https://开头").
Field("paramCustomHTTPSecret", params.ParamCustomHTTPSecret).
Require("请输入私钥")
apiParams["url"] = params.ParamCustomHTTPURL
apiParams["secret"] = params.ParamCustomHTTPSecret
default:
this.Fail("暂时不支持此服务商'" + params.Type + "'")
}

View File

@@ -0,0 +1,23 @@
package tasks
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type CheckAction struct {
actionutils.ParentAction
}
func (this *CheckAction) RunPost(params struct{}) {
resp, err := this.RPC().DNSTaskRPC().ExistsDNSTasks(this.AdminContext(), &pb.ExistsDNSTasksRequest{})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["isDoing"] = resp.ExistTasks
this.Data["hasError"] = resp.ExistError
this.Success()
}

View File

@@ -0,0 +1,24 @@
package tasks
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type DeleteAction struct {
actionutils.ParentAction
}
func (this *DeleteAction) RunPost(params struct {
TaskId int64
}) {
defer this.CreateLogInfo("删除DNS同步任务 %d", params.TaskId)
_, err := this.RPC().DNSTaskRPC().DeleteDNSTask(this.AdminContext(), &pb.DeleteDNSTaskRequest{DnsTaskId: params.TaskId})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,22 @@
package tasks
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/clusterutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeDNS)).
Helper(clusterutils.NewClustersHelper()).
Prefix("/dns/tasks").
GetPost("/listPopup", new(ListPopupAction)).
Post("/check", new(CheckAction)).
Post("/delete", new(DeleteAction)).
EndAll()
})
}

View File

@@ -0,0 +1,84 @@
package tasks
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type ListPopupAction struct {
actionutils.ParentAction
}
func (this *ListPopupAction) Init() {
this.Nav("", "", "")
}
func (this *ListPopupAction) RunGet(params struct{}) {
this.retrieveTasks()
this.Show()
}
func (this *ListPopupAction) RunPost(params struct {
Must *actions.Must
}) {
this.retrieveTasks()
this.Success()
}
func (this *ListPopupAction) retrieveTasks() {
resp, err := this.RPC().DNSTaskRPC().FindAllDoingDNSTasks(this.AdminContext(), &pb.FindAllDoingDNSTasksRequest{})
if err != nil {
this.ErrorPage(err)
return
}
taskMaps := []maps.Map{}
for _, task := range resp.DnsTasks {
var clusterMap maps.Map = nil
var nodeMap maps.Map = nil
var serverMap maps.Map = nil
var domainMap maps.Map = nil
if task.NodeCluster != nil {
clusterMap = maps.Map{
"id": task.NodeCluster.Id,
"name": task.NodeCluster.Name,
}
}
if task.Node != nil {
nodeMap = maps.Map{
"id": task.Node.Id,
"name": task.Node.Name,
}
}
if task.Server != nil {
serverMap = maps.Map{
"id": task.Server.Id,
"name": task.Server.Name,
}
}
if task.DnsDomain != nil {
domainMap = maps.Map{
"id": task.DnsDomain.Id,
"name": task.DnsDomain.Name,
}
}
taskMaps = append(taskMaps, maps.Map{
"id": task.Id,
"type": task.Type,
"isDone": task.IsDone,
"isOk": task.IsOk,
"error": task.Error,
"updatedTime": timeutil.FormatTime("Y-m-d H:i:s", task.UpdatedAt),
"cluster": clusterMap,
"node": nodeMap,
"server": serverMap,
"domain": domainMap,
})
}
this.Data["tasks"] = taskMaps
}

View File

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

View File

@@ -6,6 +6,7 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
@@ -28,7 +29,8 @@ func (this *CreatePopupAction) RunPost(params struct {
Type string
// file
FileDir string
FileDir string
FileMemoryCapacityJSON []byte
CapacityJSON []byte
MaxSizeJSON []byte
@@ -50,8 +52,21 @@ func (this *CreatePopupAction) RunPost(params struct {
params.Must.
Field("fileDir", params.FileDir).
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{
Dir: params.FileDir,
MemoryPolicy: &serverconfigs.HTTPCachePolicy{
Capacity: memoryCapacity,
},
}
case serverconfigs.CachePolicyStorageMemory:
options = &serverconfigs.HTTPMemoryCacheStorage{

View File

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

View File

@@ -48,6 +48,7 @@ func init() {
GetPost("/ipadmin/createIPPopup", new(ipadmin.CreateIPPopupAction)).
GetPost("/ipadmin/updateIPPopup", new(ipadmin.UpdateIPPopupAction)).
Post("/ipadmin/deleteIP", new(ipadmin.DeleteIPAction)).
GetPost("/ipadmin/test", new(ipadmin.TestAction)).
EndAll()
})

View File

@@ -21,7 +21,7 @@ func (this *CreateIPPopupAction) RunGet(params struct {
FirewallPolicyId int64
Type string
}) {
this.Data["type"] = params.Type
this.Data["listType"] = params.Type
listId, err := dao.SharedHTTPFirewallPolicyDAO.FindEnabledPolicyIPListIdWithType(this.AdminContext(), params.FirewallPolicyId, params.Type)
if err != nil {
@@ -40,41 +40,57 @@ func (this *CreateIPPopupAction) RunPost(params struct {
IpTo string
ExpiredAt int64
Reason string
Type string
EventLevel string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
// TODO 校验ListId所属用户
params.Must.
Field("ipFrom", params.IpFrom).
Require("请输入开始IP")
switch params.Type {
case "ipv4":
params.Must.
Field("ipFrom", params.IpFrom).
Require("请输入开始IP")
// 校验IP格式ipFrom/ipTo
ipFromLong := utils.IP2Long(params.IpFrom)
if len(params.IpFrom) > 0 {
if ipFromLong == 0 {
// 校验IP格式ipFrom/ipTo
var ipFromLong uint64
if !utils.IsIPv4(params.IpFrom) {
this.Fail("请输入正确的开始IP")
}
}
ipFromLong = utils.IP2Long(params.IpFrom)
ipToLong := utils.IP2Long(params.IpTo)
if len(params.IpTo) > 0 {
if ipToLong == 0 {
var ipToLong uint64
if len(params.IpTo) > 0 && !utils.IsIPv4(params.IpTo) {
ipToLong = utils.IP2Long(params.IpTo)
this.Fail("请输入正确的结束IP")
}
}
if ipFromLong > 0 && ipToLong > 0 && ipFromLong > ipToLong {
params.IpTo, params.IpFrom = params.IpFrom, params.IpTo
if ipFromLong > 0 && ipToLong > 0 && ipFromLong > ipToLong {
params.IpTo, params.IpFrom = params.IpFrom, params.IpTo
}
case "ipv6":
params.Must.
Field("ipFrom", params.IpFrom).
Require("请输入IP")
// 校验IP格式ipFrom
if !utils.IsIPv6(params.IpFrom) {
this.Fail("请输入正确的IPv6地址")
}
case "all":
params.IpFrom = "0.0.0.0"
}
createResp, err := this.RPC().IPItemRPC().CreateIPItem(this.AdminContext(), &pb.CreateIPItemRequest{
IpListId: params.ListId,
IpFrom: params.IpFrom,
IpTo: params.IpTo,
ExpiredAt: params.ExpiredAt,
Reason: params.Reason,
IpListId: params.ListId,
IpFrom: params.IpFrom,
IpTo: params.IpTo,
ExpiredAt: params.ExpiredAt,
Reason: params.Reason,
Type: params.Type,
EventLevel: params.EventLevel,
})
if err != nil {
this.ErrorPage(err)

View File

@@ -4,6 +4,7 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
@@ -58,11 +59,13 @@ func (this *ListsAction) RunGet(params struct {
}
itemMaps = append(itemMaps, maps.Map{
"id": item.Id,
"ipFrom": item.IpFrom,
"ipTo": item.IpTo,
"expiredTime": expiredTime,
"reason": item.Reason,
"id": item.Id,
"ipFrom": item.IpFrom,
"ipTo": item.IpTo,
"expiredTime": expiredTime,
"reason": item.Reason,
"type": item.Type,
"eventLevelName": firewallconfigs.FindFirewallEventLevelName(item.EventLevel),
})
}
this.Data["items"] = itemMaps

View File

@@ -0,0 +1,87 @@
package ipadmin
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type TestAction struct {
actionutils.ParentAction
}
func (this *TestAction) Init() {
this.Nav("", "", "ipadmin")
}
func (this *TestAction) RunGet(params struct {
FirewallPolicyId int64
}) {
this.Data["subMenuItem"] = "test"
this.Show()
}
func (this *TestAction) RunPost(params struct {
FirewallPolicyId int64
Ip string
Must *actions.Must
}) {
resp, err := this.RPC().HTTPFirewallPolicyRPC().CheckHTTPFirewallPolicyIPStatus(this.AdminContext(), &pb.CheckHTTPFirewallPolicyIPStatusRequest{
HttpFirewallPolicyId: params.FirewallPolicyId,
Ip: params.Ip,
})
if err != nil {
this.ErrorPage(err)
return
}
resultMap := maps.Map{
"isDone": true,
"isFound": resp.IsFound,
"isOk": resp.IsOk,
"error": resp.Error,
"isAllowed": resp.IsAllowed,
}
if resp.IpList != nil {
resultMap["list"] = maps.Map{
"id": resp.IpList.Id,
"name": resp.IpList.Name,
}
}
if resp.IpItem != nil {
resultMap["item"] = maps.Map{
"id": resp.IpItem.Id,
"ipFrom": resp.IpItem.IpFrom,
"ipTo": resp.IpItem.IpTo,
"reason": resp.IpItem.Reason,
"expiredAt": resp.IpItem.ExpiredAt,
"expiredTime": timeutil.FormatTime("Y-m-d H:i:s", resp.IpItem.ExpiredAt),
"type": resp.IpItem.Type,
"eventLevelName": firewallconfigs.FindFirewallEventLevelName(resp.IpItem.EventLevel),
}
}
if resp.RegionCountry != nil {
resultMap["country"] = maps.Map{
"id": resp.RegionCountry.Id,
"name": resp.RegionCountry.Name,
}
}
if resp.RegionProvince != nil {
resultMap["province"] = maps.Map{
"id": resp.RegionProvince.Id,
"name": resp.RegionProvince.Name,
}
}
this.Data["result"] = resultMap
this.Success()
}

View File

@@ -32,13 +32,17 @@ func (this *UpdateIPPopupAction) RunGet(params struct {
}
this.Data["item"] = maps.Map{
"id": item.Id,
"ipFrom": item.IpFrom,
"ipTo": item.IpTo,
"expiredAt": item.ExpiredAt,
"reason": item.Reason,
"id": item.Id,
"ipFrom": item.IpFrom,
"ipTo": item.IpTo,
"expiredAt": item.ExpiredAt,
"reason": item.Reason,
"type": item.Type,
"eventLevel": item.EventLevel,
}
this.Data["type"] = item.Type
this.Show()
}
@@ -46,10 +50,12 @@ func (this *UpdateIPPopupAction) RunPost(params struct {
FirewallPolicyId int64
ItemId int64
IpFrom string
IpTo string
ExpiredAt int64
Reason string
IpFrom string
IpTo string
ExpiredAt int64
Reason string
Type string
EventLevel string
Must *actions.Must
CSRF *actionutils.CSRF
@@ -59,35 +65,49 @@ func (this *UpdateIPPopupAction) RunPost(params struct {
// TODO 校验ItemId所属用户
params.Must.
Field("ipFrom", params.IpFrom).
Require("请输入开始IP")
switch params.Type {
case "ipv4":
params.Must.
Field("ipFrom", params.IpFrom).
Require("请输入开始IP")
// 校验IP格式ipFrom/ipTo
ipFromLong := utils.IP2Long(params.IpFrom)
if len(params.IpFrom) > 0 {
if ipFromLong == 0 {
// 校验IP格式ipFrom/ipTo
var ipFromLong uint64
if !utils.IsIPv4(params.IpFrom) {
this.Fail("请输入正确的开始IP")
}
}
ipFromLong = utils.IP2Long(params.IpFrom)
ipToLong := utils.IP2Long(params.IpTo)
if len(params.IpTo) > 0 {
if ipToLong == 0 {
var ipToLong uint64
if len(params.IpTo) > 0 && !utils.IsIPv4(params.IpTo) {
ipToLong = utils.IP2Long(params.IpTo)
this.Fail("请输入正确的结束IP")
}
}
if ipFromLong > 0 && ipToLong > 0 && ipFromLong > ipToLong {
params.IpTo, params.IpFrom = params.IpFrom, params.IpTo
if ipFromLong > 0 && ipToLong > 0 && ipFromLong > ipToLong {
params.IpTo, params.IpFrom = params.IpFrom, params.IpTo
}
case "ipv6":
params.Must.
Field("ipFrom", params.IpFrom).
Require("请输入IP")
// 校验IP格式ipFrom
if !utils.IsIPv6(params.IpFrom) {
this.Fail("请输入正确的IPv6地址")
}
case "all":
params.IpFrom = "0.0.0.0"
}
_, err := this.RPC().IPItemRPC().UpdateIPItem(this.AdminContext(), &pb.UpdateIPItemRequest{
IpItemId: params.ItemId,
IpFrom: params.IpFrom,
IpTo: params.IpTo,
ExpiredAt: params.ExpiredAt,
Reason: params.Reason,
IpItemId: params.ItemId,
IpFrom: params.IpFrom,
IpTo: params.IpTo,
ExpiredAt: params.ExpiredAt,
Reason: params.Reason,
Type: params.Type,
EventLevel: params.EventLevel,
})
if err != nil {
this.ErrorPage(err)

View File

@@ -10,6 +10,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"strings"
)
type CreateAction struct {
@@ -215,6 +216,22 @@ func (this *CreateAction) RunPost(params struct {
if err != nil {
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

@@ -86,6 +86,7 @@ func (this *SettingAction) RunPost(params struct {
RequestURI: reverseProxyConfig.RequestURI,
StripPrefix: reverseProxyConfig.StripPrefix,
AutoFlush: reverseProxyConfig.AutoFlush,
AddHeaders: reverseProxyConfig.AddHeaders,
})
this.Success()

View File

@@ -86,6 +86,7 @@ func (this *SettingAction) RunPost(params struct {
RequestURI: reverseProxyConfig.RequestURI,
StripPrefix: reverseProxyConfig.StripPrefix,
AutoFlush: reverseProxyConfig.AutoFlush,
AddHeaders: reverseProxyConfig.AddHeaders,
})
this.Success()

View File

@@ -70,10 +70,10 @@ func (this *UpdateSchedulingPopupAction) RunGet(params struct {
if !types.IsSlice(networks) {
continue
}
if (serverConfig.IsHTTP() && lists.Contains(networks, "http")) ||
(serverConfig.IsTCP() && lists.Contains(networks, "tcp")) ||
(serverConfig.IsUDP() && lists.Contains(networks, "udp")) ||
(serverConfig.IsUnix() && lists.Contains(networks, "unix")) {
if (serverConfig.IsHTTPFamily() && lists.Contains(networks, "http")) ||
(serverConfig.IsTCPFamily() && lists.Contains(networks, "tcp")) ||
(serverConfig.IsUDPFamily() && lists.Contains(networks, "udp")) ||
(serverConfig.IsUnixFamily() && lists.Contains(networks, "unix")) {
schedulingTypes = append(schedulingTypes, m)
}
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
"strings"
)
// 域名管理
@@ -74,6 +75,34 @@ func (this *IndexAction) RunPost(params struct {
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{
ServerId: params.ServerId,
ServerNamesJSON: []byte(params.ServerNames),

View File

@@ -16,6 +16,7 @@ func (this *GroupsAction) Init() {
}
func (this *GroupsAction) RunGet(params struct {
ServerId int64
FirewallPolicyId int64
Type string
}) {
@@ -70,5 +71,13 @@ func (this *GroupsAction) RunGet(params struct {
this.Data["groups"] = groupMaps
// WAF是否启用
webConfig, err := dao.SharedHTTPWebDAO.FindWebConfigWithServerId(this.AdminContext(), params.ServerId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["wafIsOn"] = webConfig.FirewallRef != nil && webConfig.FirewallRef.IsOn
this.Show()
}

View File

@@ -17,11 +17,12 @@ func init() {
GetPost("", new(IndexAction)).
Get("/ipadmin/allowList", new(ipadmin.AllowListAction)).
Get("/ipadmin/denyList", new(ipadmin.DenyListAction)).
//GetPost("/ipadmin", new(ipadmin.IndexAction)).
//GetPost("/ipadmin/provinces", new(ipadmin.ProvincesAction)).
GetPost("/ipadmin/countries", new(ipadmin.CountriesAction)).
GetPost("/ipadmin/provinces", new(ipadmin.ProvincesAction)).
GetPost("/ipadmin/createIPPopup", new(ipadmin.CreateIPPopupAction)).
GetPost("/ipadmin/updateIPPopup", new(ipadmin.UpdateIPPopupAction)).
Post("/ipadmin/deleteIP", new(ipadmin.DeleteIPAction)).
GetPost("/ipadmin/test", new(ipadmin.TestAction)).
// 规则相关
Get("/groups", new(GroupsAction)).

View File

@@ -4,8 +4,10 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
"time"
)
type AllowListAction struct {
@@ -69,14 +71,25 @@ func (this *AllowListAction) RunGet(params struct {
}
itemMaps = append(itemMaps, maps.Map{
"id": item.Id,
"ipFrom": item.IpFrom,
"ipTo": item.IpTo,
"expiredTime": expiredTime,
"reason": item.Reason,
"id": item.Id,
"ipFrom": item.IpFrom,
"ipTo": item.IpTo,
"expiredTime": expiredTime,
"reason": item.Reason,
"type": item.Type,
"isExpired": item.ExpiredAt > 0 && item.ExpiredAt < time.Now().Unix(),
"eventLevelName": firewallconfigs.FindFirewallEventLevelName(item.EventLevel),
})
}
this.Data["items"] = itemMaps
// WAF是否启用
webConfig, err := dao.SharedHTTPWebDAO.FindWebConfigWithServerId(this.AdminContext(), params.ServerId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["wafIsOn"] = webConfig.FirewallRef != nil && webConfig.FirewallRef.IsOn
this.Show()
}

View File

@@ -13,17 +13,22 @@ import (
"strings"
)
type IndexAction struct {
type CountriesAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "ipadmin")
func (this *CountriesAction) Init() {
this.Nav("", "setting", "country")
this.SecondMenu("waf")
}
func (this *IndexAction) RunGet(params struct {
func (this *CountriesAction) RunGet(params struct {
FirewallPolicyId int64
ServerId int64
}) {
this.Data["featureIsOn"] = true
this.Data["firewallPolicyId"] = params.FirewallPolicyId
this.Data["subMenuItem"] = "region"
// 当前选中的地区
@@ -57,10 +62,18 @@ func (this *IndexAction) RunGet(params struct {
}
this.Data["countries"] = countryMaps
// WAF是否启用
webConfig, err := dao.SharedHTTPWebDAO.FindWebConfigWithServerId(this.AdminContext(), params.ServerId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["wafIsOn"] = webConfig.FirewallRef != nil && webConfig.FirewallRef.IsOn
this.Show()
}
func (this *IndexAction) RunPost(params struct {
func (this *CountriesAction) RunPost(params struct {
FirewallPolicyId int64
CountryIds []int64

View File

@@ -20,53 +20,67 @@ func (this *CreateIPPopupAction) RunGet(params struct {
ListId int64
Type string
}) {
this.Data["type"] = params.Type
this.Data["listType"] = params.Type
this.Data["listId"] = params.ListId
this.Show()
}
func (this *CreateIPPopupAction) RunPost(params struct {
ListId int64
IpFrom string
IpTo string
ExpiredAt int64
Reason string
ListId int64
IpFrom string
IpTo string
ExpiredAt int64
Reason string
Type string
EventLevel string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
// TODO 校验ListId所属用户
switch params.Type {
case "ipv4":
params.Must.
Field("ipFrom", params.IpFrom).
Require("请输入开始IP")
params.Must.
Field("ipFrom", params.IpFrom).
Require("请输入开始IP")
// 校验IP格式ipFrom/ipTo
ipFromLong := utils.IP2Long(params.IpFrom)
if len(params.IpFrom) > 0 {
if ipFromLong == 0 {
// 校验IP格式ipFrom/ipTo
var ipFromLong uint64
if !utils.IsIPv4(params.IpFrom) {
this.Fail("请输入正确的开始IP")
}
}
ipFromLong = utils.IP2Long(params.IpFrom)
ipToLong := utils.IP2Long(params.IpTo)
if len(params.IpTo) > 0 {
if ipToLong == 0 {
var ipToLong uint64
if len(params.IpTo) > 0 && !utils.IsIPv4(params.IpTo) {
ipToLong = utils.IP2Long(params.IpTo)
this.Fail("请输入正确的结束IP")
}
}
if ipFromLong > 0 && ipToLong > 0 && ipFromLong > ipToLong {
params.IpTo, params.IpFrom = params.IpFrom, params.IpTo
if ipFromLong > 0 && ipToLong > 0 && ipFromLong > ipToLong {
params.IpTo, params.IpFrom = params.IpFrom, params.IpTo
}
case "ipv6":
params.Must.
Field("ipFrom", params.IpFrom).
Require("请输入IP")
// 校验IP格式ipFrom
if !utils.IsIPv6(params.IpFrom) {
this.Fail("请输入正确的IPv6地址")
}
case "all":
params.IpFrom = "0.0.0.0"
}
createResp, err := this.RPC().IPItemRPC().CreateIPItem(this.AdminContext(), &pb.CreateIPItemRequest{
IpListId: params.ListId,
IpFrom: params.IpFrom,
IpTo: params.IpTo,
ExpiredAt: params.ExpiredAt,
Reason: params.Reason,
IpListId: params.ListId,
IpFrom: params.IpFrom,
IpTo: params.IpTo,
ExpiredAt: params.ExpiredAt,
Reason: params.Reason,
Type: params.Type,
EventLevel: params.EventLevel,
})
if err != nil {
this.ErrorPage(err)

View File

@@ -4,8 +4,10 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
"time"
)
type DenyListAction struct {
@@ -69,14 +71,25 @@ func (this *DenyListAction) RunGet(params struct {
}
itemMaps = append(itemMaps, maps.Map{
"id": item.Id,
"ipFrom": item.IpFrom,
"ipTo": item.IpTo,
"expiredTime": expiredTime,
"reason": item.Reason,
"id": item.Id,
"ipFrom": item.IpFrom,
"ipTo": item.IpTo,
"expiredTime": expiredTime,
"reason": item.Reason,
"type": item.Type,
"isExpired": item.ExpiredAt > 0 && item.ExpiredAt < time.Now().Unix(),
"eventLevelName": firewallconfigs.FindFirewallEventLevelName(item.EventLevel),
})
}
this.Data["items"] = itemMaps
// WAF是否启用
webConfig, err := dao.SharedHTTPWebDAO.FindWebConfigWithServerId(this.AdminContext(), params.ServerId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["wafIsOn"] = webConfig.FirewallRef != nil && webConfig.FirewallRef.IsOn
this.Show()
}

View File

@@ -19,12 +19,16 @@ type ProvincesAction struct {
}
func (this *ProvincesAction) Init() {
this.Nav("", "", "ipadmin")
this.Nav("", "setting", "province")
this.SecondMenu("waf")
}
func (this *ProvincesAction) RunGet(params struct {
FirewallPolicyId int64
ServerId int64
}) {
this.Data["featureIsOn"] = true
this.Data["firewallPolicyId"] = params.FirewallPolicyId
this.Data["subMenuItem"] = "province"
// 当前选中的省份
@@ -59,6 +63,14 @@ func (this *ProvincesAction) RunGet(params struct {
}
this.Data["provinces"] = provinceMaps
// WAF是否启用
webConfig, err := dao.SharedHTTPWebDAO.FindWebConfigWithServerId(this.AdminContext(), params.ServerId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["wafIsOn"] = webConfig.FirewallRef != nil && webConfig.FirewallRef.IsOn
this.Show()
}

View File

@@ -0,0 +1,100 @@
package ipadmin
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type TestAction struct {
actionutils.ParentAction
}
func (this *TestAction) Init() {
this.Nav("", "setting", "test")
this.SecondMenu("waf")
}
func (this *TestAction) RunGet(params struct {
ServerId int64
FirewallPolicyId int64
}) {
this.Data["featureIsOn"] = true
this.Data["firewallPolicyId"] = params.FirewallPolicyId
this.Data["subMenuItem"] = "province"
// WAF是否启用
webConfig, err := dao.SharedHTTPWebDAO.FindWebConfigWithServerId(this.AdminContext(), params.ServerId)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["wafIsOn"] = webConfig.FirewallRef != nil && webConfig.FirewallRef.IsOn
this.Show()
}
func (this *TestAction) RunPost(params struct {
FirewallPolicyId int64
Ip string
Must *actions.Must
}) {
resp, err := this.RPC().HTTPFirewallPolicyRPC().CheckHTTPFirewallPolicyIPStatus(this.AdminContext(), &pb.CheckHTTPFirewallPolicyIPStatusRequest{
HttpFirewallPolicyId: params.FirewallPolicyId,
Ip: params.Ip,
})
if err != nil {
this.ErrorPage(err)
return
}
resultMap := maps.Map{
"isDone": true,
"isFound": resp.IsFound,
"isOk": resp.IsOk,
"error": resp.Error,
"isAllowed": resp.IsAllowed,
}
if resp.IpList != nil {
resultMap["list"] = maps.Map{
"id": resp.IpList.Id,
"name": resp.IpList.Name,
}
}
if resp.IpItem != nil {
resultMap["item"] = maps.Map{
"id": resp.IpItem.Id,
"ipFrom": resp.IpItem.IpFrom,
"ipTo": resp.IpItem.IpTo,
"reason": resp.IpItem.Reason,
"expiredAt": resp.IpItem.ExpiredAt,
"expiredTime": timeutil.FormatTime("Y-m-d H:i:s", resp.IpItem.ExpiredAt),
"type": resp.IpItem.Type,
"eventLevelName": firewallconfigs.FindFirewallEventLevelName(resp.IpItem.EventLevel),
}
}
if resp.RegionCountry != nil {
resultMap["country"] = maps.Map{
"id": resp.RegionCountry.Id,
"name": resp.RegionCountry.Name,
}
}
if resp.RegionProvince != nil {
resultMap["province"] = maps.Map{
"id": resp.RegionProvince.Id,
"name": resp.RegionProvince.Name,
}
}
this.Data["result"] = resultMap
this.Success()
}

View File

@@ -32,23 +32,29 @@ func (this *UpdateIPPopupAction) RunGet(params struct {
}
this.Data["item"] = maps.Map{
"id": item.Id,
"ipFrom": item.IpFrom,
"ipTo": item.IpTo,
"expiredAt": item.ExpiredAt,
"reason": item.Reason,
"id": item.Id,
"ipFrom": item.IpFrom,
"ipTo": item.IpTo,
"expiredAt": item.ExpiredAt,
"reason": item.Reason,
"type": item.Type,
"eventLevel": item.EventLevel,
}
this.Data["type"] = item.Type
this.Show()
}
func (this *UpdateIPPopupAction) RunPost(params struct {
ItemId int64
IpFrom string
IpTo string
ExpiredAt int64
Reason string
IpFrom string
IpTo string
ExpiredAt int64
Reason string
Type string
EventLevel string
Must *actions.Must
CSRF *actionutils.CSRF
@@ -58,35 +64,49 @@ func (this *UpdateIPPopupAction) RunPost(params struct {
// TODO 校验ItemId所属用户
params.Must.
Field("ipFrom", params.IpFrom).
Require("请输入开始IP")
switch params.Type {
case "ipv4":
params.Must.
Field("ipFrom", params.IpFrom).
Require("请输入开始IP")
// 校验IP格式ipFrom/ipTo
ipFromLong := utils.IP2Long(params.IpFrom)
if len(params.IpFrom) > 0 {
if ipFromLong == 0 {
// 校验IP格式ipFrom/ipTo
var ipFromLong uint64
if !utils.IsIPv4(params.IpFrom) {
this.Fail("请输入正确的开始IP")
}
}
ipFromLong = utils.IP2Long(params.IpFrom)
ipToLong := utils.IP2Long(params.IpTo)
if len(params.IpTo) > 0 {
if ipToLong == 0 {
var ipToLong uint64
if len(params.IpTo) > 0 && !utils.IsIPv4(params.IpTo) {
ipToLong = utils.IP2Long(params.IpTo)
this.Fail("请输入正确的结束IP")
}
}
if ipFromLong > 0 && ipToLong > 0 && ipFromLong > ipToLong {
params.IpTo, params.IpFrom = params.IpFrom, params.IpTo
if ipFromLong > 0 && ipToLong > 0 && ipFromLong > ipToLong {
params.IpTo, params.IpFrom = params.IpFrom, params.IpTo
}
case "ipv6":
params.Must.
Field("ipFrom", params.IpFrom).
Require("请输入IP")
// 校验IP格式ipFrom
if !utils.IsIPv6(params.IpFrom) {
this.Fail("请输入正确的IPv6地址")
}
case "all":
params.IpFrom = "0.0.0.0"
}
_, err := this.RPC().IPItemRPC().UpdateIPItem(this.AdminContext(), &pb.UpdateIPItemRequest{
IpItemId: params.ItemId,
IpFrom: params.IpFrom,
IpTo: params.IpTo,
ExpiredAt: params.ExpiredAt,
Reason: params.Reason,
IpItemId: params.ItemId,
IpFrom: params.IpFrom,
IpTo: params.IpTo,
ExpiredAt: params.ExpiredAt,
Reason: params.Reason,
Type: params.Type,
EventLevel: params.EventLevel,
})
if err != nil {
this.ErrorPage(err)

View File

@@ -0,0 +1,110 @@
package stat
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type ClientsAction struct {
actionutils.ParentAction
}
func (this *ClientsAction) Init() {
this.Nav("", "stat", "")
this.SecondMenu("client")
}
func (this *ClientsAction) RunGet(params struct {
ServerId int64
Month string
}) {
month := params.Month
if len(month) != 6 {
month = timeutil.Format("Ym")
}
this.Data["month"] = month
serverTypeResp, err := this.RPC().ServerRPC().FindEnabledServerType(this.AdminContext(), &pb.FindEnabledServerTypeRequest{ServerId: params.ServerId})
if err != nil {
this.ErrorPage(err)
return
}
serverType := serverTypeResp.Type
statIsOn := false
// 是否已开启
if serverconfigs.IsHTTPServerType(serverType) {
webConfig, err := dao.SharedHTTPWebDAO.FindWebConfigWithServerId(this.AdminContext(), params.ServerId)
if err != nil {
this.ErrorPage(err)
return
}
if webConfig != nil && webConfig.StatRef != nil {
statIsOn = webConfig.StatRef.IsOn
}
} else {
this.WriteString("此类型服务暂不支持统计")
return
}
this.Data["statIsOn"] = statIsOn
// 统计数据
systemMaps := []maps.Map{}
browserMaps := []maps.Map{}
if statIsOn {
{
resp, err := this.RPC().ServerClientSystemMonthlyStatRPC().FindTopServerClientSystemMonthlyStats(this.AdminContext(), &pb.FindTopServerClientSystemMonthlyStatsRequest{
ServerId: params.ServerId,
Month: month,
Offset: 0,
Size: 10,
})
if err != nil {
this.ErrorPage(err)
return
}
for _, stat := range resp.Stats {
systemMaps = append(systemMaps, maps.Map{
"count": stat.Count,
"system": maps.Map{
"id": stat.ClientSystem.Id,
"name": stat.ClientSystem.Name + " " + stat.Version,
},
})
}
}
{
resp, err := this.RPC().ServerClientBrowserMonthlyStatRPC().FindTopServerClientBrowserMonthlyStats(this.AdminContext(), &pb.FindTopServerClientBrowserMonthlyStatsRequest{
ServerId: params.ServerId,
Month: month,
Offset: 0,
Size: 10,
})
if err != nil {
this.ErrorPage(err)
return
}
for _, stat := range resp.Stats {
browserMaps = append(browserMaps, maps.Map{
"count": stat.Count,
"browser": maps.Map{
"id": stat.ClientBrowser.Id,
"name": stat.ClientBrowser.Name + " " + stat.Version,
},
})
}
}
}
this.Data["systemStats"] = systemMaps
this.Data["browserStats"] = browserMaps
this.Show()
}

View File

@@ -1,6 +1,13 @@
package stat
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type IndexAction struct {
actionutils.ParentAction
@@ -11,6 +18,133 @@ func (this *IndexAction) Init() {
this.SecondMenu("index")
}
func (this *IndexAction) RunGet(params struct{}) {
func (this *IndexAction) RunGet(params struct {
ServerId int64
Month string
}) {
month := params.Month
if len(month) != 6 {
month = timeutil.Format("Ym")
}
this.Data["month"] = month
serverTypeResp, err := this.RPC().ServerRPC().FindEnabledServerType(this.AdminContext(), &pb.FindEnabledServerTypeRequest{ServerId: params.ServerId})
if err != nil {
this.ErrorPage(err)
return
}
serverType := serverTypeResp.Type
statIsOn := false
// 是否已开启
if serverconfigs.IsHTTPServerType(serverType) {
webConfig, err := dao.SharedHTTPWebDAO.FindWebConfigWithServerId(this.AdminContext(), params.ServerId)
if err != nil {
this.ErrorPage(err)
return
}
if webConfig != nil && webConfig.StatRef != nil {
statIsOn = webConfig.StatRef.IsOn
}
} else {
this.WriteString("此类型服务暂不支持统计")
return
}
this.Data["statIsOn"] = statIsOn
// 统计数据
countryStatMaps := []maps.Map{}
provinceStatMaps := []maps.Map{}
cityStatMaps := []maps.Map{}
if statIsOn {
// 地区
{
resp, err := this.RPC().ServerRegionCountryMonthlyStatRPC().FindTopServerRegionCountryMonthlyStats(this.AdminContext(), &pb.FindTopServerRegionCountryMonthlyStatsRequest{
Month: month,
ServerId: params.ServerId,
Offset: 0,
Size: 10,
})
if err != nil {
this.ErrorPage(err)
return
}
for _, stat := range resp.Stats {
countryStatMaps = append(countryStatMaps, maps.Map{
"count": stat.Count,
"country": maps.Map{
"id": stat.RegionCountry.Id,
"name": stat.RegionCountry.Name,
},
})
}
}
// 省份
{
resp, err := this.RPC().ServerRegionProvinceMonthlyStatRPC().FindTopServerRegionProvinceMonthlyStats(this.AdminContext(), &pb.FindTopServerRegionProvinceMonthlyStatsRequest{
Month: month,
ServerId: params.ServerId,
Offset: 0,
Size: 10,
})
if err != nil {
this.ErrorPage(err)
return
}
for _, stat := range resp.Stats {
provinceStatMaps = append(provinceStatMaps, maps.Map{
"count": stat.Count,
"country": maps.Map{
"id": stat.RegionCountry.Id,
"name": stat.RegionCountry.Name,
},
"province": maps.Map{
"id": stat.RegionProvince.Id,
"name": stat.RegionProvince.Name,
},
})
}
}
// 城市
{
resp, err := this.RPC().ServerRegionCityMonthlyStatRPC().FindTopServerRegionCityMonthlyStats(this.AdminContext(), &pb.FindTopServerRegionCityMonthlyStatsRequest{
Month: month,
ServerId: params.ServerId,
Offset: 0,
Size: 10,
})
if err != nil {
this.ErrorPage(err)
return
}
for _, stat := range resp.Stats {
cityStatMaps = append(cityStatMaps, maps.Map{
"count": stat.Count,
"country": maps.Map{
"id": stat.RegionCountry.Id,
"name": stat.RegionCountry.Name,
},
"province": maps.Map{
"id": stat.RegionProvince.Id,
"name": stat.RegionProvince.Name,
},
"city": maps.Map{
"id": stat.RegionCity.Id,
"name": stat.RegionCity.Name,
},
})
}
}
}
this.Data["countryStats"] = countryStatMaps
this.Data["provinceStats"] = provinceStatMaps
this.Data["cityStats"] = cityStatMaps
this.Show()
}

View File

@@ -14,6 +14,9 @@ func init() {
Helper(serverutils.NewServerHelper()).
Prefix("/servers/server/stat").
Get("", new(IndexAction)).
Get("/providers", new(ProvidersAction)).
Get("/clients", new(ClientsAction)).
Get("/waf", new(WafAction)).
EndAll()
})
}

View File

@@ -0,0 +1,86 @@
package stat
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type ProvidersAction struct {
actionutils.ParentAction
}
func (this *ProvidersAction) Init() {
this.Nav("", "stat", "")
this.SecondMenu("provider")
}
func (this *ProvidersAction) RunGet(params struct {
ServerId int64
Month string
}) {
month := params.Month
if len(month) != 6 {
month = timeutil.Format("Ym")
}
this.Data["month"] = month
serverTypeResp, err := this.RPC().ServerRPC().FindEnabledServerType(this.AdminContext(), &pb.FindEnabledServerTypeRequest{ServerId: params.ServerId})
if err != nil {
this.ErrorPage(err)
return
}
serverType := serverTypeResp.Type
statIsOn := false
// 是否已开启
if serverconfigs.IsHTTPServerType(serverType) {
webConfig, err := dao.SharedHTTPWebDAO.FindWebConfigWithServerId(this.AdminContext(), params.ServerId)
if err != nil {
this.ErrorPage(err)
return
}
if webConfig != nil && webConfig.StatRef != nil {
statIsOn = webConfig.StatRef.IsOn
}
} else {
this.WriteString("此类型服务暂不支持统计")
return
}
this.Data["statIsOn"] = statIsOn
// 统计数据
providerMaps := []maps.Map{}
if statIsOn {
{
resp, err := this.RPC().ServerRegionProviderMonthlyStatRPC().FindTopServerRegionProviderMonthlyStats(this.AdminContext(), &pb.FindTopServerRegionProviderMonthlyStatsRequest{
Month: month,
ServerId: params.ServerId,
Offset: 0,
Size: 10,
})
if err != nil {
this.ErrorPage(err)
return
}
for _, stat := range resp.Stats {
providerMaps = append(providerMaps, maps.Map{
"count": stat.Count,
"provider": maps.Map{
"id": stat.RegionProvider.Id,
"name": stat.RegionProvider.Name,
},
})
}
}
}
this.Data["providerStats"] = providerMaps
this.Show()
}

View File

@@ -0,0 +1,78 @@
package stat
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type WafAction struct {
actionutils.ParentAction
}
func (this *WafAction) Init() {
this.Nav("", "stat", "")
this.SecondMenu("waf")
}
func (this *WafAction) RunGet(params struct {
ServerId int64
}) {
// 统计数据
resp, err := this.RPC().ServerHTTPFirewallDailyStatRPC().ComposeServerHTTPFirewallDashboard(this.AdminContext(), &pb.ComposeServerHTTPFirewallDashboardRequest{
Day: timeutil.Format("Ymd"),
ServerId: params.ServerId,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["countDailyLog"] = resp.CountDailyLog
this.Data["countDailyBlock"] = resp.CountDailyBlock
this.Data["countDailyCaptcha"] = resp.CountDailyCaptcha
this.Data["countWeeklyBlock"] = resp.CountWeeklyBlock
this.Data["countMonthlyBlock"] = resp.CountMonthlyBlock
// 分组
groupStatMaps := []maps.Map{}
for _, group := range resp.HttpFirewallRuleGroups {
groupStatMaps = append(groupStatMaps, maps.Map{
"group": maps.Map{
"id": group.HttpFirewallRuleGroup.Id,
"name": group.HttpFirewallRuleGroup.Name,
},
"count": group.Count,
})
}
this.Data["groupStats"] = groupStatMaps
// 每日趋势
logStatMaps := []maps.Map{}
blockStatMaps := []maps.Map{}
captchaStatMaps := []maps.Map{}
for _, stat := range resp.LogDailyStats {
logStatMaps = append(logStatMaps, maps.Map{
"day": stat.Day,
"count": stat.Count,
})
}
for _, stat := range resp.BlockDailyStats {
blockStatMaps = append(blockStatMaps, maps.Map{
"day": stat.Day,
"count": stat.Count,
})
}
for _, stat := range resp.CaptchaDailyStats {
captchaStatMaps = append(captchaStatMaps, maps.Map{
"day": stat.Day,
"count": stat.Count,
})
}
this.Data["logDailyStats"] = logStatMaps
this.Data["blockDailyStats"] = blockStatMaps
this.Data["captchaDailyStats"] = captchaStatMaps
this.Show()
}

View File

@@ -77,13 +77,13 @@ func (this *ServerHelper) createLeftMenu(action *actions.ActionObject) {
// 协议簇
family := ""
if serverConfig.IsHTTP() {
if serverConfig.IsHTTPFamily() {
family = "http"
} else if serverConfig.IsTCP() {
} else if serverConfig.IsTCPFamily() {
family = "tcp"
} else if serverConfig.IsUnix() {
} else if serverConfig.IsUnixFamily() {
family = "unix"
} else if serverConfig.IsUDP() {
} else if serverConfig.IsUDPFamily() {
family = "udp"
}
action.Data["serverFamily"] = family
@@ -94,7 +94,9 @@ func (this *ServerHelper) createLeftMenu(action *actions.ActionObject) {
tabbar.Add("服务列表", "", "/servers", "", false)
//tabbar.Add("看板", "", "/servers/server/board?serverId="+serverIdString, "dashboard", selectedTabbar == "board")
tabbar.Add("日志", "", "/servers/server/log?serverId="+serverIdString, "history", selectedTabbar == "log")
//tabbar.Add("统计", "", "/servers/server/stat?serverId="+serverIdString, "chart area", selectedTabbar == "stat")
if family == "http" {
tabbar.Add("统计", "", "/servers/server/stat?serverId="+serverIdString, "chart area", selectedTabbar == "stat")
}
tabbar.Add("设置", "", "/servers/server/settings?serverId="+serverIdString, "setting", selectedTabbar == "setting")
tabbar.Add("删除", "", "/servers/server/delete?serverId="+serverIdString, "trash", selectedTabbar == "delete")
{
@@ -155,10 +157,25 @@ func (this *ServerHelper) createLogMenu(secondMenuItem string, serverIdString st
func (this *ServerHelper) createStatMenu(secondMenuItem string, serverIdString string, serverConfig *serverconfigs.ServerConfig) []maps.Map {
menuItems := []maps.Map{}
menuItems = append(menuItems, maps.Map{
"name": "统计",
"name": "地域分布",
"url": "/servers/server/stat?serverId=" + serverIdString,
"isActive": secondMenuItem == "index",
})
menuItems = append(menuItems, maps.Map{
"name": "运营商",
"url": "/servers/server/stat/providers?serverId=" + serverIdString,
"isActive": secondMenuItem == "provider",
})
menuItems = append(menuItems, maps.Map{
"name": "终端",
"url": "/servers/server/stat/clients?serverId=" + serverIdString,
"isActive": secondMenuItem == "client",
})
menuItems = append(menuItems, maps.Map{
"name": "WAF",
"url": "/servers/server/stat/waf?serverId=" + serverIdString,
"isActive": secondMenuItem == "waf",
})
return menuItems
}
@@ -179,7 +196,7 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
}
// HTTP
if serverConfig.IsHTTP() {
if serverConfig.IsHTTPFamily() {
menuItems = append(menuItems, maps.Map{
"name": "域名",
"url": "/servers/server/settings/serverNames?serverId=" + serverIdString,
@@ -294,7 +311,7 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
"isActive": secondMenuItem == "websocket",
"isOn": serverConfig.Web != nil && serverConfig.Web.WebsocketRef != nil && serverConfig.Web.WebsocketRef.IsOn,
})
} else if serverConfig.IsTCP() {
} else if serverConfig.IsTCPFamily() {
menuItems = append(menuItems, maps.Map{
"name": "TCP",
"url": "/servers/server/settings/tcp?serverId=" + serverIdString,
@@ -313,14 +330,14 @@ func (this *ServerHelper) createSettingsMenu(secondMenuItem string, serverIdStri
"isActive": secondMenuItem == "reverseProxy",
"isOn": serverConfig.ReverseProxyRef != nil && serverConfig.ReverseProxyRef.IsOn,
})
} else if serverConfig.IsUnix() {
} else if serverConfig.IsUnixFamily() {
menuItems = append(menuItems, maps.Map{
"name": "Unix",
"url": "/servers/server/settings/unix?serverId=" + serverIdString,
"isActive": secondMenuItem == "unix",
"isOn": serverConfig.Unix != nil && serverConfig.Unix.IsOn && len(serverConfig.Unix.Listen) > 0,
})
} else if serverConfig.IsUDP() {
} else if serverConfig.IsUDPFamily() {
menuItems = append(menuItems, maps.Map{
"name": "UDP",
"url": "/servers/server/settings/udp?serverId=" + serverIdString,

View File

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

View File

@@ -3,7 +3,9 @@ package ui
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"io"
)
type IndexAction struct {
@@ -32,10 +34,14 @@ func (this *IndexAction) RunPost(params struct {
ShowFinance bool
ShowVersion bool
Version string
FaviconFile *actions.File
LogoFile *actions.File
Must *actions.Must
CSRF *actionutils.CSRF
}) {
defer this.CreateLogInfo("修改管理界面设置")
params.Must.
Field("productName", params.ProductName).
Require("请输入产品名称").
@@ -53,6 +59,101 @@ func (this *IndexAction) RunPost(params struct {
config.ShowFinance = params.ShowFinance
config.ShowVersion = params.ShowVersion
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)
if err != nil {
this.ErrorPage(err)

View File

@@ -3,7 +3,9 @@ package userui
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"io"
)
type IndexAction struct {
@@ -32,6 +34,8 @@ func (this *IndexAction) RunPost(params struct {
ShowVersion bool
Version string
ShowFinance bool
FaviconFile *actions.File
LogoFile *actions.File
Must *actions.Must
CSRF *actionutils.CSRF
@@ -53,6 +57,101 @@ func (this *IndexAction) RunPost(params struct {
config.ShowVersion = params.ShowVersion
config.Version = params.Version
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)
if err != nil {
this.ErrorPage(err)

View File

@@ -1,6 +1,7 @@
package ui
import (
"bytes"
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/servers/server/settings/conds/condutils"
"github.com/iwind/TeaGo/Tea"
@@ -11,9 +12,19 @@ import (
type ComponentsAction actions.Action
var componentsData = []byte{}
func (this *ComponentsAction) RunGet(params struct{}) {
this.AddHeader("Content-Type", "text/javascript; charset=utf-8")
if !Tea.IsTesting() && len(componentsData) > 0 {
this.AddHeader("Last-Modified", "Fri, 06 Sep 2019 08:29:50 GMT")
this.Write(componentsData)
return
}
var buffer = bytes.NewBuffer([]byte{})
var webRoot string
if Tea.IsTesting() {
webRoot = Tea.Root + "/../web/public/js/components/"
@@ -34,8 +45,8 @@ func (this *ComponentsAction) RunGet(params struct{}) {
logs.Error(err)
return
}
this.Write(data)
this.Write([]byte{'\n', '\n'})
buffer.Write(data)
buffer.Write([]byte{'\n', '\n'})
})
// 条件组件
@@ -43,8 +54,11 @@ func (this *ComponentsAction) RunGet(params struct{}) {
if err != nil {
logs.Println("ComponentsAction: " + err.Error())
} else {
this.WriteString("window.REQUEST_COND_COMPONENTS = ")
this.Write(typesJSON)
this.Write([]byte{'\n', '\n'})
buffer.WriteString("window.REQUEST_COND_COMPONENTS = ")
buffer.Write(typesJSON)
buffer.Write([]byte{'\n', '\n'})
}
componentsData = buffer.Bytes()
this.Write(componentsData)
}

View File

@@ -0,0 +1,16 @@
package ui
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
)
type EventLevelOptionsAction struct {
actionutils.ParentAction
}
func (this *EventLevelOptionsAction) RunPost(params struct{}) {
this.Data["eventLevels"] = firewallconfigs.FindAllFirewallEventLevels()
this.Success()
}

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.
Prefix("/ui").
// 公共可以访问的链接
Get("/image/:fileId", new(ImageAction)).
// 以下的需要压缩
Helper(&actions.Gzip{Level: gzip.BestCompression}).
Get("/components.js", new(ComponentsAction)).
@@ -23,6 +26,7 @@ func init() {
Get("/download", new(DownloadAction)).
GetPost("/selectProvincesPopup", new(SelectProvincesPopupAction)).
GetPost("/selectCountriesPopup", new(SelectCountriesPopupAction)).
Post("/eventLevelOptions", new(EventLevelOptionsAction)).
EndAll()
})

View File

@@ -88,6 +88,8 @@ func (this *userMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam
action.Data["teaShowVersion"] = config.ShowVersion
action.Data["teaTitle"] = config.AdminSystemName
action.Data["teaName"] = config.ProductName
action.Data["teaFaviconFileId"] = config.FaviconFileId
action.Data["teaLogoFileId"] = config.LogoFileId
action.Data["teaUsername"] = configloaders.FindAdminFullname(adminId)
action.Data["teaUserAvatar"] = ""
@@ -110,7 +112,8 @@ func (this *userMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam
if !action.Data.Has("teaSubMenu") {
action.Data["teaSubMenu"] = ""
}
action.Data["teaCheckClusterTask"] = configloaders.AllowModule(adminId, configloaders.AdminModuleCodeNode)
action.Data["teaCheckNodeTasks"] = configloaders.AllowModule(adminId, configloaders.AdminModuleCodeNode)
action.Data["teaCheckDNSTasks"] = configloaders.AllowModule(adminId, configloaders.AdminModuleCodeDNS)
// 菜单
action.Data["firstMenuItem"] = ""
@@ -130,6 +133,12 @@ func (this *userMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam
// 菜单配置
func (this *userMustAuth) modules(adminId int64) []maps.Map {
allMaps := []maps.Map{
{
"code": "dashboard",
"module": configloaders.AdminModuleCodeDashboard,
"name": "数据看板",
"icon": "dashboard",
},
{
"code": "servers",
"module": configloaders.AdminModuleCodeServer,

View File

@@ -17,6 +17,7 @@ import (
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dashboard"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/db"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dns"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dns/tasks"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/finance"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/finance/bills"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/index"

View File

@@ -29,6 +29,13 @@ Vue.component("checkbox", {
this.$emit("input", this.newValue)
}
},
watch: {
value: function (v) {
if (typeof v == "boolean") {
this.newValue = v
}
}
},
template: `<div class="ui checkbox">
<input type="checkbox" :name="name" :value="elementValue" :id="elementId" @change="change" v-model="newValue"/>
<label :for="elementId" style="font-size: 0.85em!important;"><slot></slot></label>

View File

@@ -1,144 +1,154 @@
Vue.component("datetime-input", {
props: ["v-name", "v-timestamp"],
data: function () {
let timestamp = this.vTimestamp
if (timestamp != null) {
timestamp = parseInt(timestamp)
if (isNaN(timestamp)) {
timestamp = 0
}
} else {
timestamp = 0
}
props: ["v-name", "v-timestamp"],
mounted: function () {
let that = this
teaweb.datepicker(this.$refs.dayInput, function (v) {
that.day = v
that.hour = "23"
that.minute = "59"
that.second = "59"
that.change()
})
},
data: function () {
let timestamp = this.vTimestamp
if (timestamp != null) {
timestamp = parseInt(timestamp)
if (isNaN(timestamp)) {
timestamp = 0
}
} else {
timestamp = 0
}
let day = ""
let hour = ""
let minute = ""
let second = ""
let day = ""
let hour = ""
let minute = ""
let second = ""
if (timestamp > 0) {
let date = new Date()
date.setTime(timestamp * 1000)
if (timestamp > 0) {
let date = new Date()
date.setTime(timestamp * 1000)
let year = date.getFullYear().toString()
let month = this.leadingZero((date.getMonth() + 1).toString(), 2)
day = year + "-" + month + "-" + this.leadingZero(date.getDate().toString(), 2)
let year = date.getFullYear().toString()
let month = this.leadingZero((date.getMonth() + 1).toString(), 2)
day = year + "-" + month + "-" + this.leadingZero(date.getDate().toString(), 2)
hour = this.leadingZero(date.getHours().toString(), 2)
minute = this.leadingZero(date.getMinutes().toString(), 2)
second = this.leadingZero(date.getSeconds().toString(), 2)
}
hour = this.leadingZero(date.getHours().toString(), 2)
minute = this.leadingZero(date.getMinutes().toString(), 2)
second = this.leadingZero(date.getSeconds().toString(), 2)
}
return {
timestamp: timestamp,
day: day,
hour: hour,
minute: minute,
second: second,
return {
timestamp: timestamp,
day: day,
hour: hour,
minute: minute,
second: second,
hasDayError: false,
hasHourError: false,
hasMinuteError: false,
hasSecondError: false
}
},
methods: {
change: function () {
let date = new Date()
hasDayError: false,
hasHourError: false,
hasMinuteError: false,
hasSecondError: false
}
},
methods: {
change: function () {
let date = new Date()
// day
if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(this.day)) {
this.hasDayError = true
return
}
let pieces = this.day.split("-")
let year = parseInt(pieces[0])
date.setFullYear(year)
// day
if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(this.day)) {
this.hasDayError = true
return
}
let pieces = this.day.split("-")
let year = parseInt(pieces[0])
date.setFullYear(year)
let month = parseInt(pieces[1])
if (month < 1 || month > 12) {
this.hasDayError = true
return
}
date.setMonth(month - 1)
let month = parseInt(pieces[1])
if (month < 1 || month > 12) {
this.hasDayError = true
return
}
date.setMonth(month - 1)
let day = parseInt(pieces[2])
if (day < 1 || day > 32) {
this.hasDayError = true
return
}
date.setDate(day)
let day = parseInt(pieces[2])
if (day < 1 || day > 32) {
this.hasDayError = true
return
}
date.setDate(day)
this.hasDayError = false
this.hasDayError = false
// hour
if (!/^\d+$/.test(this.hour)) {
this.hasHourError = true
return
}
let hour = parseInt(this.hour)
if (isNaN(hour)) {
this.hasHourError = true
return
}
if (hour < 0 || hour >= 24) {
this.hasHourError = true
return
}
this.hasHourError = false
date.setHours(hour)
// hour
if (!/^\d+$/.test(this.hour)) {
this.hasHourError = true
return
}
let hour = parseInt(this.hour)
if (isNaN(hour)) {
this.hasHourError = true
return
}
if (hour < 0 || hour >= 24) {
this.hasHourError = true
return
}
this.hasHourError = false
date.setHours(hour)
// minute
if (!/^\d+$/.test(this.minute)) {
this.hasMinuteError = true
return
}
let minute = parseInt(this.minute)
if (isNaN(minute)) {
this.hasMinuteError = true
return
}
if (minute < 0 || minute >= 60) {
this.hasMinuteError = true
return
}
this.hasMinuteError = false
date.setMinutes(minute)
// minute
if (!/^\d+$/.test(this.minute)) {
this.hasMinuteError = true
return
}
let minute = parseInt(this.minute)
if (isNaN(minute)) {
this.hasMinuteError = true
return
}
if (minute < 0 || minute >= 60) {
this.hasMinuteError = true
return
}
this.hasMinuteError = false
date.setMinutes(minute)
// second
if (!/^\d+$/.test(this.second)) {
this.hasSecondError = true
return
}
let second = parseInt(this.second)
if (isNaN(second)) {
this.hasSecondError = true
return
}
if (second < 0 || second >= 60) {
this.hasSecondError = true
return
}
this.hasSecondError = false
date.setSeconds(second)
// second
if (!/^\d+$/.test(this.second)) {
this.hasSecondError = true
return
}
let second = parseInt(this.second)
if (isNaN(second)) {
this.hasSecondError = true
return
}
if (second < 0 || second >= 60) {
this.hasSecondError = true
return
}
this.hasSecondError = false
date.setSeconds(second)
this.timestamp = Math.ceil(date.getTime() / 1000)
},
leadingZero: function (s, l) {
if (l <= s.length) {
return s
}
for (let i = 0; i < l - s.length; i++) {
s = "0" + s
}
return s
}
},
template: `<div>
this.timestamp = parseInt(date.getTime() / 1000)
},
leadingZero: function (s, l) {
if (l <= s.length) {
return s
}
for (let i = 0; i < l - s.length; i++) {
s = "0" + s
}
return s
}
},
template: `<div>
<input type="hidden" :name="vName" :value="timestamp"/>
<div class="ui fields inline" style="padding: 0; margin:0">
<div class="ui field" :class="{error: hasDayError}">
<input type="text" v-model="day" placeholder="YYYY-mm-dd" style="width:8em" maxlength="10" @input="change"/>
<input type="text" v-model="day" placeholder="YYYY-MM-DD" style="width:8.6em" maxlength="10" @input="change" ref="dayInput"/>
</div>
<div class="ui field" :class="{error: hasHourError}"><input type="text" v-model="hour" maxlength="2" style="width:4em" placeholder="时" @input="change"/></div>
<div class="ui field">:</div>

View File

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

View File

@@ -0,0 +1,12 @@
Vue.component("ip-item-text", {
props: ["v-item"],
template: `<span>
<span v-if="vItem.type == 'all'">*</span>
<span v-if="vItem.type == 'ipv4' || vItem.type.length == 0">
{{vItem.ipFrom}}
<span v-if="vItem.ipTo.length > 0">- {{vItem.ipTo}}</span>
</span>
<span v-if="vItem.type == 'ipv6'">{{vItem.ipFrom}}</span>
<span v-if="vItem.eventLevelName != null && vItem.eventLevelName.length > 0">&nbsp; 级别:{{vItem.eventLevelName}}</span>
</span>`
})

View File

@@ -0,0 +1,63 @@
Vue.component("ip-list-table", {
props: ["v-items"],
data: function () {
return {
items: this.vItems
}
},
methods: {
updateItem: function (itemId) {
this.$emit("update-item", itemId)
},
deleteItem: function (itemId) {
this.$emit("delete-item", itemId)
}
},
template: `<div>
<table class="ui table selectable celled" v-if="items.length > 0">
<thead>
<tr>
<th style="width:18em">IP</th>
<th>类型</th>
<th>级别</th>
<th>过期时间</th>
<th>备注</th>
<th class="two op">操作</th>
</tr>
</thead>
<tr v-for="item in items">
<td>
<span v-if="item.type != 'all'">{{item.ipFrom}}<span v-if="item.ipTo.length > 0"> - {{item.ipTo}}</span></span>
<span v-else class="disabled">*</span>
</td>
<td>
<span v-if="item.type.length == 0">IPv4</span>
<span v-else-if="item.type == 'ipv4'">IPv4</span>
<span v-else-if="item.type == 'ipv6'">IPv6</span>
<span v-else-if="item.type == 'all'"><strong>所有IP</strong></span>
</td>
<td>
<span v-if="item.eventLevelName != null && item.eventLevelName.length > 0">{{item.eventLevelName}}</span>
<span v-else class="disabled">-</span>
</td>
<td>
<div v-if="item.expiredTime.length > 0">
{{item.expiredTime}}
<div v-if="item.isExpired" style="margin-top: 0.5em">
<span class="ui label tiny basic red">已过期</span>
</div>
</div>
<span v-else class="disabled">不过期</span>
</td>
<td>
<span v-if="item.reason.length > 0">{{item.reason}}</span>
<span v-else class="disabled">-</span>
</td>
<td>
<a href="" @click.prevent="updateItem(item.id)">修改</a> &nbsp;
<a href="" @click.prevent="deleteItem(item.id)">删除</a>
</td>
</tr>
</table>
</div>`
})

View File

@@ -1,38 +1,53 @@
Vue.component("message-row", {
props: ["v-message"],
data: function () {
let paramsJSON = this.vMessage.params
let params = null
if (paramsJSON != null && paramsJSON.length > 0) {
params = JSON.parse(paramsJSON)
}
props: ["v-message"],
data: function () {
let paramsJSON = this.vMessage.params
let params = null
if (paramsJSON != null && paramsJSON.length > 0) {
params = JSON.parse(paramsJSON)
}
return {
message: this.vMessage,
params: params
}
},
methods: {
viewCert: function (certId) {
teaweb.popup("/servers/certs/certPopup?certId=" + certId, {
height: "28em",
width: "48em"
})
}
},
template: `<div>
return {
message: this.vMessage,
params: params
}
},
methods: {
viewCert: function (certId) {
teaweb.popup("/servers/certs/certPopup?certId=" + certId, {
height: "28em",
width: "48em"
})
},
readMessage: function (messageId) {
Tea.action("/messages/readPage")
.params({"messageIds": [messageId]})
.post()
.success(function () {
// 刷新父级页面Badge
if (window.parent.Tea != null && window.parent.Tea.Vue != null) {
window.parent.Tea.Vue.checkMessagesOnce()
}
// 刷新当前页面
teaweb.reload()
})
}
},
template: `<div>
<table class="ui table selectable">
<tr :class="{error: message.level == 'error', positive: message.level == 'success', warning: message.level == 'warning'}">
<td>
<td style="position: relative">
<strong>{{message.datetime}}</strong>
<span v-if="message.cluster != null && message.cluster.id != null">
<span> | </span>
<a :href="'/clusters/cluster?clusterId=' + message.cluster.id">集群:{{message.cluster.name}}</a>
<a :href="'/clusters/cluster?clusterId=' + message.cluster.id" target="_top">集群:{{message.cluster.name}}</a>
</span>
<span v-if="message.node != null && message.node.id != null">
<span> | </span>
<a :href="'/clusters/cluster/node?clusterId=' + message.cluster.id + '&nodeId=' + message.node.id">节点:{{message.node.name}}</a>
<a :href="'/clusters/cluster/node?clusterId=' + message.cluster.id + '&nodeId=' + message.node.id" target="_top">节点:{{message.node.name}}</a>
</span>
<a href="" style="position: absolute; right: 1em" @click.prevent="readMessage(message.id)" title="标为已读"><i class="icon check"></i></a>
</td>
</tr>
<tr :class="{error: message.level == 'error', positive: message.level == 'success', warning: message.level == 'warning'}">
@@ -41,25 +56,25 @@ Vue.component("message-row", {
<!-- 健康检查 -->
<div v-if="message.type == 'HealthCheckFailed'" style="margin-top: 0.8em">
<a :href="'/clusters/cluster/node?clusterId=' + message.cluster.id + '&nodeId=' + param.node.id" v-for="param in params" class="ui label small basic" style="margin-bottom: 0.5em">{{param.node.name}}: {{param.error}}</a>
<a :href="'/clusters/cluster/node?clusterId=' + message.cluster.id + '&nodeId=' + param.node.id" v-for="param in params" class="ui label small basic" style="margin-bottom: 0.5em" target="_top">{{param.node.name}}: {{param.error}}</a>
</div>
<!-- 集群DNS设置 -->
<div v-if="message.type == 'ClusterDNSSyncFailed'" style="margin-top: 0.8em">
<a :href="'/dns/clusters/cluster?clusterId=' + message.cluster.id">查看问题 &raquo;</a>
<a :href="'/dns/clusters/cluster?clusterId=' + message.cluster.id" target="_top">查看问题 &raquo;</a>
</div>
<!-- 证书即将过期 -->
<div v-if="message.type == 'SSLCertExpiring'" style="margin-top: 0.8em">
<a href="" @click.prevent="viewCert(params.certId)">查看证书</a> &nbsp;|&nbsp; <a :href="'/servers/certs/acme'" v-if="params != null && params.acmeTaskId > 0">查看任务&raquo;</a>
<a href="" @click.prevent="viewCert(params.certId)" target="_top">查看证书</a> &nbsp;|&nbsp; <a :href="'/servers/certs/acme'" v-if="params != null && params.acmeTaskId > 0" target="_top">查看任务&raquo;</a>
</div>
<!-- 证书续期成功 -->
<div v-if="message.type == 'SSLCertACMETaskSuccess'" style="margin-top: 0.8em">
<a href="" @click.prevent="viewCert(params.certId)">查看证书</a> &nbsp;|&nbsp; <a :href="'/servers/certs/acme'" v-if="params != null && params.acmeTaskId > 0">查看任务&raquo;</a>
<a href="" @click.prevent="viewCert(params.certId)" target="_top">查看证书</a> &nbsp;|&nbsp; <a :href="'/servers/certs/acme'" v-if="params != null && params.acmeTaskId > 0" target="_top">查看任务&raquo;</a>
</div>
<div v-if="message.type == 'SSLCertACMETaskFailed'" style="margin-top: 0.8em">
<a href="" @click.prevent="viewCert(params.certId)">查看证书</a> &nbsp;|&nbsp; <a :href="'/servers/certs/acme'" v-if="params != null && params.acmeTaskId > 0">查看任务&raquo;</a>
<a href="" @click.prevent="viewCert(params.certId)" target="_top">查看证书</a> &nbsp;|&nbsp; <a :href="'/servers/certs/acme'" v-if="params != null && params.acmeTaskId > 0" target="_top">查看任务&raquo;</a>
</div>
</td>
</tr>

View File

@@ -0,0 +1,45 @@
Vue.component("firewall-event-level-options", {
props: ["v-value"],
mounted: function () {
let that = this
Tea.action("/ui/eventLevelOptions")
.post()
.success(function (resp) {
that.levels = resp.data.eventLevels
that.change()
})
},
data: function () {
let value = this.vValue
if (value == null || value.length == 0) {
value = "" // 不要给默认值,因为黑白名单等默认值均有不同
}
return {
levels: [],
description: "",
level: value
}
},
methods: {
change: function () {
this.$emit("change")
let that = this
let l = this.levels.$find(function (k, v) {
return v.code == that.level
})
if (l != null) {
this.description = l.description
} else {
this.description = ""
}
}
},
template: `<div>
<select class="ui dropdown auto-width" name="eventLevel" v-model="level" @change="change">
<option v-for="level in levels" :value="level.code">{{level.name}}</option>
</select>
<p class="comment">{{description}}</p>
</div>`
})

View File

@@ -20,7 +20,9 @@ Vue.component("http-access-log-config-box", {
status5: true,
storageOnly: false,
storagePolicies: []
storagePolicies: [],
firewallOnly: false
}
if (this.vAccessLogConfig != null) {
accessLog = this.vAccessLogConfig
@@ -132,6 +134,19 @@ Vue.component("http-access-log-config-box", {
</tr>
</tbody>
</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>`
})

View File

@@ -5,7 +5,7 @@ Vue.component("http-stat-config-box", {
if (stat == null) {
stat = {
isPrior: false,
isOn: true
isOn: false
}
}
return {

View File

@@ -14,7 +14,7 @@ Vue.component("http-websocket-box", {
if (websocketConfig == null) {
websocketConfig = {
id: 0,
isOn: true,
isOn: false,
handshakeTimeout: {
count: 30,
unit: "second"

View File

@@ -1,50 +1,88 @@
Vue.component("reverse-proxy-box", {
props: ["v-reverse-proxy-ref", "v-reverse-proxy-config", "v-is-location", "v-family"],
data: function () {
let reverseProxyRef = this.vReverseProxyRef
if (reverseProxyRef == null) {
reverseProxyRef = {
isPrior: false,
isOn: false,
reverseProxyId: 0
}
}
props: ["v-reverse-proxy-ref", "v-reverse-proxy-config", "v-is-location", "v-family"],
data: function () {
let reverseProxyRef = this.vReverseProxyRef
if (reverseProxyRef == null) {
reverseProxyRef = {
isPrior: false,
isOn: false,
reverseProxyId: 0
}
}
let reverseProxyConfig = this.vReverseProxyConfig
if (reverseProxyConfig == null) {
reverseProxyConfig = {
requestPath: "",
stripPrefix: "",
requestURI: "",
requestHost: "",
requestHostType: 0
}
}
return {
reverseProxyRef: reverseProxyRef,
reverseProxyConfig: reverseProxyConfig,
advancedVisible: false,
family: this.vFamily
}
},
watch: {
"reverseProxyConfig.requestHostType": function (v) {
let requestHostType = parseInt(v)
if (isNaN(requestHostType)) {
requestHostType = 0
}
this.reverseProxyConfig.requestHostType = requestHostType
}
},
methods: {
isOn: function () {
return (!this.vIsLocation || this.reverseProxyRef.isPrior) && this.reverseProxyRef.isOn
},
changeAdvancedVisible: function (v) {
this.advancedVisible = v
}
},
template: `<div>
let reverseProxyConfig = this.vReverseProxyConfig
if (reverseProxyConfig == null) {
reverseProxyConfig = {
requestPath: "",
stripPrefix: "",
requestURI: "",
requestHost: "",
requestHostType: 0,
addHeaders: []
}
} else if (reverseProxyConfig.addHeaders == null) {
reverseProxyConfig.addHeaders = []
}
let forwardHeaders = [
{
name: "X-Real-IP",
isChecked: false
},
{
name: "X-Forwarded-For",
isChecked: false
},
{
name: "X-Forwarded-By",
isChecked: false
},
{
name: "X-Forwarded-Host",
isChecked: false
},
{
name: "X-Forwarded-Proto",
isChecked: false
}
]
forwardHeaders.forEach(function (v) {
v.isChecked = reverseProxyConfig.addHeaders.$contains(v.name)
})
return {
reverseProxyRef: reverseProxyRef,
reverseProxyConfig: reverseProxyConfig,
advancedVisible: false,
family: this.vFamily,
forwardHeaders: forwardHeaders
}
},
watch: {
"reverseProxyConfig.requestHostType": function (v) {
let requestHostType = parseInt(v)
if (isNaN(requestHostType)) {
requestHostType = 0
}
this.reverseProxyConfig.requestHostType = requestHostType
}
},
methods: {
isOn: function () {
return (!this.vIsLocation || this.reverseProxyRef.isPrior) && this.reverseProxyRef.isOn
},
changeAdvancedVisible: function (v) {
this.advancedVisible = v
},
changeAddHeader: function () {
this.reverseProxyConfig.addHeaders = this.forwardHeaders.filter(function (v) {
return v.isChecked
}).map(function (v) {
return v.name
})
}
},
template: `<div>
<input type="hidden" name="reverseProxyRefJSON" :value="JSON.stringify(reverseProxyRef)"/>
<input type="hidden" name="reverseProxyJSON" :value="JSON.stringify(reverseProxyConfig)"/>
<table class="ui table selectable definition">
@@ -77,6 +115,18 @@ Vue.component("reverse-proxy-box", {
</tbody>
<more-options-tbody @change="changeAdvancedVisible" v-if="isOn()"></more-options-tbody>
<tbody v-show="isOn() && advancedVisible">
<tr v-show="family == null || family == 'http'">
<td>自动添加的Header</td>
<td>
<div>
<div style="width: 14em; float: left; margin-bottom: 1em" v-for="header in forwardHeaders" :key="header.name">
<checkbox v-model="header.isChecked" @input="changeAddHeader">{{header.name}}</checkbox>
</div>
<div style="clear: both"></div>
</div>
<p class="comment">选中后会自动向源站请求添加这些Header。</p>
</td>
</tr>
<tr v-show="family == null || family == 'http'">
<td>请求URI<em>RequestURI</em></td>
<td>

16
web/public/js/echarts/echarts.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,261 +1,297 @@
window.teaweb = {
set: function (key, value) {
localStorage.setItem(key, JSON.stringify(value));
},
get: function (key) {
var item = localStorage.getItem(key);
if (item == null || item.length == 0) {
return null;
}
set: function (key, value) {
localStorage.setItem(key, JSON.stringify(value));
},
get: function (key) {
var item = localStorage.getItem(key);
if (item == null || item.length == 0) {
return null;
}
return JSON.parse(item);
},
getString: function (key) {
var value = this.get(key);
if (typeof (value) == "string") {
return value;
}
return "";
},
getBool: function (key) {
return Boolean(this.get(key));
},
remove: function (key) {
localStorage.removeItem(key)
},
match: function (source, keyword) {
if (source == null) {
return false;
}
if (keyword == null) {
return true;
}
source = source.trim();
keyword = keyword.trim();
if (keyword.length == 0) {
return true;
}
if (source.length == 0) {
return false;
}
var pieces = keyword.split(/\s+/);
for (var i = 0; i < pieces.length; i++) {
var pattern = pieces[i];
pattern = pattern.replace(/(\+|\*|\?|[|]|{|}|\||\\|\(|\)|\.)/g, "\\$1");
var reg = new RegExp(pattern, "i");
if (!reg.test(source)) {
return false;
}
}
return true;
},
return JSON.parse(item);
},
getString: function (key) {
var value = this.get(key);
if (typeof (value) == "string") {
return value;
}
return "";
},
getBool: function (key) {
return Boolean(this.get(key));
},
remove: function (key) {
localStorage.removeItem(key)
},
match: function (source, keyword) {
if (source == null) {
return false;
}
if (keyword == null) {
return true;
}
source = source.trim();
keyword = keyword.trim();
if (keyword.length == 0) {
return true;
}
if (source.length == 0) {
return false;
}
var pieces = keyword.split(/\s+/);
for (var i = 0; i < pieces.length; i++) {
var pattern = pieces[i];
pattern = pattern.replace(/(\+|\*|\?|[|]|{|}|\||\\|\(|\)|\.)/g, "\\$1");
var reg = new RegExp(pattern, "i");
if (!reg.test(source)) {
return false;
}
}
return true;
},
datepicker: function (element, callback) {
if (typeof (element) == "string") {
element = document.getElementById(element);
}
var year = new Date().getFullYear();
var picker = new Pikaday({
field: element,
firstDay: 1,
minDate: new Date(year - 1, 0, 1),
maxDate: new Date(year + 10, 11, 31),
yearRange: [year - 1, year + 10],
format: "YYYY-MM-DD",
i18n: {
previousMonth: '上月',
nextMonth: '下月',
months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
weekdays: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
weekdaysShort: ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
},
theme: 'triangle-theme',
onSelect: function () {
if (typeof (callback) == "function") {
callback.call(Tea.Vue, picker.toString());
}
}
});
},
loadJS: function (file, callback) {
let element = document.createElement("script")
element.setAttribute("type", "text/javascript")
element.setAttribute("src", file)
if (typeof callback == "function") {
element.addEventListener("load", callback)
}
document.head.append(element)
},
loadCSS: function (file, callback) {
let element = document.createElement("link")
element.setAttribute("rel", "stylesheet")
element.setAttribute("type", "text/css")
element.setAttribute("href", file)
if (typeof callback == "function") {
element.addEventListener("load", callback)
}
document.head.append(element)
},
datepicker: function (element, callback) {
// 加载
if (typeof Pikaday == "undefined") {
let that = this
this.loadJS("/js/moment.min.js")
this.loadJS("/js/pikaday.js", function () {
that.datepicker(element, callback)
})
this.loadCSS("/js/pikaday.css")
this.loadCSS("/js/pikaday.theme.css")
this.loadCSS("/js/pikaday.triangle.css")
formatBytes: function (bytes) {
bytes = Math.ceil(bytes);
if (bytes < 1024) {
return bytes + " bytes";
}
if (bytes < 1024 * 1024) {
return (Math.ceil(bytes * 100 / 1024) / 100) + " k";
}
return (Math.ceil(bytes * 100 / 1024 / 1024) / 100) + " m";
},
return
}
popup: function (url, options) {
if (options == null) {
options = {};
}
var width = "40em";
var height = "20em";
window.POPUP_CALLBACK = function () {
Swal.close();
};
if (options["width"] != null) {
width = options["width"];
}
if (options["height"] != null) {
height = options["height"];
}
if (typeof (options["callback"]) == "function") {
window.POPUP_CALLBACK = function () {
Swal.close();
options["callback"].apply(Tea.Vue, arguments);
};
}
if (typeof (element) == "string") {
element = document.getElementById(element);
}
var year = new Date().getFullYear();
var picker = new Pikaday({
field: element,
firstDay: 1,
minDate: new Date(year - 1, 0, 1),
maxDate: new Date(year + 10, 11, 31),
yearRange: [year - 1, year + 10],
format: "YYYY-MM-DD",
i18n: {
previousMonth: '上月',
nextMonth: '下月',
months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
weekdays: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
weekdaysShort: ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
},
theme: 'triangle-theme',
onSelect: function () {
if (typeof (callback) == "function") {
callback.call(Tea.Vue, picker.toString());
}
}
});
},
Swal.fire({
html: '<iframe src="' + url + '#popup-' + width + '" style="border:0; width: 100%; height:' + height + '"></iframe>',
width: width,
padding: "0.5em",
showConfirmButton: false,
showCloseButton: true,
focusConfirm: false,
onClose: function (popup) {
if (typeof (options["onClose"]) == "function") {
options["onClose"].apply(Tea.Vue, arguments)
}
}
});
},
popupFinish: function () {
if (window.POPUP_CALLBACK != null) {
window.POPUP_CALLBACK.apply(window, arguments);
}
},
popupTip: function (html) {
Swal.fire({
html: '<i class="icon question circle"></i><span style="line-height: 1.7">' + html + "</span>",
width: "30em",
padding: "5em",
showConfirmButton: false,
showCloseButton: true,
focusConfirm: false
});
},
isPopup: function () {
var hash = window.location.hash;
return hash != null && hash.startsWith("#popup");
},
Swal: function () {
return this.isPopup() ? window.parent.Swal : window.Swal;
},
success: function (message, callback) {
var width = "20em";
if (message.length > 30) {
width = "30em";
}
formatBytes: function (bytes) {
bytes = Math.ceil(bytes);
if (bytes < 1024) {
return bytes + " bytes";
}
if (bytes < 1024 * 1024) {
return (Math.ceil(bytes * 100 / 1024) / 100) + " k";
}
return (Math.ceil(bytes * 100 / 1024 / 1024) / 100) + " m";
},
formatNumber: function (x) {
return x.toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ", ");
},
popup: function (url, options) {
if (options == null) {
options = {};
}
var width = "40em";
var height = "20em";
window.POPUP_CALLBACK = function () {
Swal.close();
};
let config = {
confirmButtonText: "确定",
buttonsStyling: false,
icon: "success",
customClass: {
closeButton: "ui button",
cancelButton: "ui button",
confirmButton: "ui button primary"
},
width: width,
onAfterClose: function () {
if (typeof (callback) == "function") {
setTimeout(function () {
callback();
});
} else if (typeof (callback) == "string") {
window.location = callback
}
}
}
if (options["width"] != null) {
width = options["width"];
}
if (options["height"] != null) {
height = options["height"];
}
if (typeof (options["callback"]) == "function") {
window.POPUP_CALLBACK = function () {
Swal.close();
options["callback"].apply(Tea.Vue, arguments);
};
}
if (message.startsWith("html:")) {
config.html = message.substring(5)
} else {
config.text = message
}
Swal.fire({
html: '<iframe src="' + url + '#popup-' + width + '" style="border:0; width: 100%; height:' + height + '"></iframe>',
width: width,
padding: "0.5em",
showConfirmButton: false,
showCloseButton: true,
focusConfirm: false,
onClose: function (popup) {
if (typeof (options["onClose"]) == "function") {
options["onClose"].apply(Tea.Vue, arguments)
}
}
});
},
popupFinish: function () {
if (window.POPUP_CALLBACK != null) {
window.POPUP_CALLBACK.apply(window, arguments);
}
},
popupTip: function (html) {
Swal.fire({
html: '<i class="icon question circle"></i><span style="line-height: 1.7">' + html + "</span>",
width: "30em",
padding: "5em",
showConfirmButton: false,
showCloseButton: true,
focusConfirm: false
});
},
isPopup: function () {
var hash = window.location.hash;
return hash != null && hash.startsWith("#popup");
},
Swal: function () {
return this.isPopup() ? window.parent.Swal : window.Swal;
},
success: function (message, callback) {
var width = "20em";
if (message.length > 30) {
width = "30em";
}
Swal.fire(config);
},
successToast: function (message, timeout) {
if (timeout == null) {
timeout = 2000
}
var width = "20em";
if (message.length > 30) {
width = "30em";
}
Swal.fire({
text: message,
icon: "success",
width: width,
timer: timeout,
showConfirmButton: false
});
},
warn: function (message, callback) {
var width = "20em";
if (message.length > 30) {
width = "30em";
}
Swal.fire({
text: message,
confirmButtonText: "确定",
buttonsStyling: false,
customClass: {
closeButton: "ui button",
cancelButton: "ui button",
confirmButton: "ui button primary"
},
icon: "warning",
width: width,
onAfterClose: function () {
if (typeof (callback) == "function") {
setTimeout(function () {
callback();
});
}
}
});
},
confirm: function (message, callback) {
let width = "20em";
if (message.length > 30) {
width = "30em";
}
let config = {
confirmButtonText: "确定",
cancelButtonText: "取消",
showCancelButton: true,
showCloseButton: false,
buttonsStyling: false,
customClass: {
closeButton: "ui button",
cancelButton: "ui button",
confirmButton: "ui button primary"
},
icon: "warning",
width: width,
preConfirm: function () {
if (typeof (callback) == "function") {
callback.call(Tea.Vue);
}
}
}
if (message.startsWith("html:")) {
config.html = message.substring(5)
} else {
config.text = message
}
Swal.fire(config);
},
reload: function () {
window.location.reload()
}
let config = {
confirmButtonText: "确定",
buttonsStyling: false,
icon: "success",
customClass: {
closeButton: "ui button",
cancelButton: "ui button",
confirmButton: "ui button primary"
},
width: width,
onAfterClose: function () {
if (typeof (callback) == "function") {
setTimeout(function () {
callback();
});
} else if (typeof (callback) == "string") {
window.location = callback
}
}
}
if (message.startsWith("html:")) {
config.html = message.substring(5)
} else {
config.text = message
}
Swal.fire(config);
},
successToast: function (message, timeout) {
if (timeout == null) {
timeout = 2000
}
var width = "20em";
if (message.length > 30) {
width = "30em";
}
Swal.fire({
text: message,
icon: "success",
width: width,
timer: timeout,
showConfirmButton: false
});
},
warn: function (message, callback) {
var width = "20em";
if (message.length > 30) {
width = "30em";
}
Swal.fire({
text: message,
confirmButtonText: "确定",
buttonsStyling: false,
customClass: {
closeButton: "ui button",
cancelButton: "ui button",
confirmButton: "ui button primary"
},
icon: "warning",
width: width,
onAfterClose: function () {
if (typeof (callback) == "function") {
setTimeout(function () {
callback();
});
}
}
});
},
confirm: function (message, callback) {
let width = "20em";
if (message.length > 30) {
width = "30em";
}
let config = {
confirmButtonText: "确定",
cancelButtonText: "取消",
showCancelButton: true,
showCloseButton: false,
buttonsStyling: false,
customClass: {
closeButton: "ui button",
cancelButton: "ui button",
confirmButton: "ui button primary"
},
icon: "warning",
width: width,
preConfirm: function () {
if (typeof (callback) == "function") {
callback.call(Tea.Vue);
}
}
}
if (message.startsWith("html:")) {
config.html = message.substring(5)
} else {
config.text = message
}
Swal.fire(config);
},
reload: function () {
window.location.reload()
}
};

View File

@@ -2,7 +2,7 @@
width: 8em;
position: fixed;
top: 7.5em;
bottom: 0.5em;
bottom: 2.4em;
overflow-y: auto;
overflow-x: hidden;
border-right: 1px #ddd solid;
@@ -77,6 +77,9 @@
.right-box.without-tabbar {
top: 3em;
}
.main.without-footer .left-box {
bottom: 0.2em;
}
/** 通用 **/
.clear {
clear: both;
@@ -217,9 +220,20 @@ p.margin {
opacity: 0.8;
}
}
@keyframes rotation {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
body .ui.menu .item .blink {
animation: blink 1s infinite;
}
body .ui.menu .item:not(:hover) span.rotate {
animation: rotation 3s infinite;
}
body.expanded .main-menu {
display: none;
}
@@ -234,10 +248,7 @@ body.expanded .main {
z-index: 1000;
overflow-x: auto;
border: 0 !important;
background: #276ac6 !important;
}
.top-nav::-webkit-scrollbar {
height: 2px;
background: #14539A !important;
}
.top-nav img.avatar {
width: 1.6em !important;
@@ -252,9 +263,18 @@ body.expanded .main {
font-size: 0.9em;
padding-left: 0.2em;
}
.top-nav .item .hover-span span {
display: none;
}
.top-nav .item:hover .hover-span span {
display: inline;
}
.top-nav .item.red {
color: red !important;
}
.top-nav::-webkit-scrollbar {
height: 2px;
}
/** 顶部菜单 **/
.top-secondary-menu {
position: fixed;
@@ -365,11 +385,11 @@ body.expanded .main {
top: 2em;
bottom: 0;
overflow-y: auto;
background: #276ac6 !important;
background: #14539A !important;
z-index: 10;
}
.main-menu .menu {
background: #276ac6 !important;
background: #14539A !important;
border: 0 !important;
box-shadow: none !important;
}

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,11 @@
<title>{$.teaTitle}</title>
<meta charset="UTF-8"/>
<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"/>
{$else}
<link rel="shortcut icon" href="/ui/image/{$.teaFaviconFileId}"/>
{$end}
<link rel="stylesheet" type="text/css" href="/_/@default/@layout.css" media="all"/>
{$TEA.SEMANTIC}
<link rel="stylesheet" type="text/css" href="/_/@default/@layout_override.css" media="all"/>
@@ -23,19 +27,29 @@
<!-- 顶部导航 -->
<div class="ui menu top-nav blue inverted small borderless" v-cloak="">
<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>
<div class="right menu">
<!-- 集群同步 -->
<a href="" class="item" v-if="teaCheckClusterTask && doingNodeTasks.isUpdated" @click.prevent="showNodeTasks()">
<span v-if="!doingNodeTasks.isDoing && !doingNodeTasks.hasError"><i class="icon sync"></i>已同步</span>
<span v-if="doingNodeTasks.isDoing && !doingNodeTasks.hasError"><i class="icon sync"></i>正在同步...</span>
<span v-if="doingNodeTasks.hasError" class="red"><i class="icon sync"></i>同步失败</span>
<a href="" class="item" v-if="teaCheckNodeTasks && doingNodeTasks.isUpdated" @click.prevent="showNodeTasks()">
<span v-if="!doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span"><i class="icon cloud disabled"></i><span class="disabled">已同步节点</span></span>
<span v-if="doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span rotate"><i class="icon cloud"></i><span>正在同步节点...</span></span>
<span v-if="doingNodeTasks.hasError" class="red"><i class="icon cloud"></i>节点同步失败</span>
</a>
<!-- DNS同步 -->
<a href="" class="item" v-if="teaCheckDNSTasks && doingDNSTasks.isUpdated" @click.prevent="showDNSTasks()">
<span v-if="!doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span"><i class="icon globe disabled"></i><span class="disabled">已同步DNS</span></span>
<span v-if="doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span rotate"><i class="icon globe"></i><span>正在同步DNS...</span></span>
<span v-if="doingDNSTasks.hasError" class="red"><i class="icon globe"></i>DNS同步失败</span>
</a>
<!-- 消息 -->
<a href="/messages" class="item" :class="{active:teaMenu == 'message'}"><span :class="{'blink':globalMessageBadge > 0}"><i class="icon bell"></i>消息({{globalMessageBadge}}) </span></a>
<a href="" class="item" :class="{active:teaMenu == 'message'}" @click.prevent="showMessages()">
<span v-if="globalMessageBadge > 0" class="blink hover-span"><i class="icon bell"></i><span>消息({{globalMessageBadge}}) </span></span>
<span v-if="globalMessageBadge == 0" class="hover-span"><i class="icon bell disabled"></i><span class="disabled">消息(0)</span></span>
</a>
<!-- 用户信息 -->
<a href="/settings/profile" class="item">
@@ -45,7 +59,7 @@
</a>
<!-- 退出登录 -->
<a :href="Tea.url('logout')" class="item" title="安全退出登录"><i class="icon sign out"></i>退出</a>
<a :href="Tea.url('logout')" class="item" title="安全退出登录"><i class="icon sign out"></i></a>
</div>
</div>
@@ -75,7 +89,7 @@
</div>
<!-- 右侧主操作栏 -->
<div class="main" :class="{'without-menu':teaSubMenus.menus == null || teaSubMenus.menus.length == 0 || (teaSubMenus.menus.length == 1 && teaSubMenus.menus[0].alwaysActive), 'without-secondary-menu':teaSubMenus.alwaysMenu == null || teaSubMenus.alwaysMenu.items.length <= 1}" v-cloak="">
<div class="main" :class="{'without-menu':teaSubMenus.menus == null || teaSubMenus.menus.length == 0 || (teaSubMenus.menus.length == 1 && teaSubMenus.menus[0].alwaysActive), 'without-secondary-menu':teaSubMenus.alwaysMenu == null || teaSubMenus.alwaysMenu.items.length <= 1, 'without-footer':!teaShowOpenSourceInfo}" v-cloak="">
<!-- 操作菜单 -->
<div class="ui top menu tabular tab-menu small" v-if="teaTabbar.length > 0">
<a class="item" v-for="item in teaTabbar" :class="{'active':item.active,right:item.right}" :href="item.url">

View File

@@ -16,6 +16,9 @@ Tea.context(function () {
// 检查集群节点同步
this.loadNodeTasks();
// 检查DNS同步
this.loadDNSTasks()
})
/**
@@ -56,6 +59,21 @@ Tea.context(function () {
})
}
this.checkMessagesOnce = function () {
this.$post("/messages/badge")
.params({})
.success(function (resp) {
this.globalMessageBadge = resp.data.count
})
}
this.showMessages = function () {
teaweb.popup("/messages", {
height: "24em",
width: "50em"
})
}
/**
* 底部伸展框
*/
@@ -83,6 +101,9 @@ Tea.context(function () {
}
this.loadNodeTasks = function () {
if (!Tea.Vue.teaCheckNodeTasks) {
return
}
this.$post("/clusters/tasks/check")
.success(function (resp) {
this.doingNodeTasks.isDoing = resp.data.isDoing
@@ -92,7 +113,7 @@ Tea.context(function () {
.done(function () {
this.$delay(function () {
this.loadNodeTasks()
}, 5000)
}, 3000)
})
}
@@ -102,6 +123,39 @@ Tea.context(function () {
width: "50em"
})
}
/**
* DNS同步任务
*/
this.doingDNSTasks = {
isDoing: false,
hasError: false,
isUpdated: false
}
this.loadDNSTasks = function () {
if (!Tea.Vue.teaCheckDNSTasks) {
return
}
this.$post("/dns/tasks/check")
.success(function (resp) {
this.doingDNSTasks.isDoing = resp.data.isDoing
this.doingDNSTasks.hasError = resp.data.hasError
this.doingDNSTasks.isUpdated = true
})
.done(function () {
this.$delay(function () {
this.loadDNSTasks()
}, 3000)
})
}
this.showDNSTasks = function () {
teaweb.popup("/dns/tasks/listPopup", {
height: "24em",
width: "50em"
})
}
});
window.NotifySuccess = function (message, url, params) {

View File

@@ -176,10 +176,23 @@ div.margin, p.margin {
}
}
@keyframes rotation {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
body .ui.menu .item .blink {
animation: blink 1s infinite;
}
body .ui.menu .item:not(:hover) span.rotate {
animation: rotation 3s infinite;
}
body.expanded .main-menu {
display: none;
}
@@ -196,31 +209,48 @@ body.expanded .main {
z-index: 1000;
overflow-x: auto;
border: 0 !important;
background: #276ac6 !important;
background: #14539A !important;
img.avatar {
width: 1.6em !important;
height: 1.6em !important;
padding: 0.2em;
background: #fff;
border-radius: 0.9em;
margin-right: 0.5em !important;
}
em {
font-style: normal;
font-size: 0.9em;
padding-left: 0.2em;
}
.item {
.hover-span {
span {
display: none;
}
}
}
.item:hover {
.hover-span {
span {
display: inline;
}
}
}
.item.red {
color: red !important;
}
}
.top-nav::-webkit-scrollbar {
height: 2px;
}
.top-nav img.avatar {
width: 1.6em !important;
height: 1.6em !important;
padding: 0.2em;
background: #fff;
border-radius: 0.9em;
margin-right: 0.5em !important;
}
.top-nav em {
font-style: normal;
font-size: 0.9em;
padding-left: 0.2em;
}
.top-nav .item.red {
color: red !important;
}
/** 顶部菜单 **/
.top-secondary-menu {
@@ -357,11 +387,11 @@ body.expanded .main {
top: 2em;
bottom: 0;
overflow-y: auto;
background: #276ac6 !important;
background: #14539A !important;
z-index: 10;
.menu {
background: #276ac6 !important;
background: #14539A !important;
border: 0 !important;
box-shadow: none !important;
}

View File

@@ -2,7 +2,7 @@
width: 8em;
position: fixed;
top: 7.5em;
bottom: 0.5em;
bottom: 2.4em;
overflow-y: auto;
overflow-x: hidden;
border-right: 1px #ddd solid;
@@ -102,3 +102,9 @@
.right-box.without-tabbar {
top: 3em;
}
// main
.main.without-footer .left-box {
bottom: 0.2em;
}

View File

@@ -24,7 +24,7 @@
<td>
<input type="hidden" name="dnsDomainId" :value="dnsDomainId"/>
<dns-route-selector :v-all-routes="dnsRoutes"></dns-route-selector>
<p class="comment">可用线路是根据集群设置的域名获取的注意DNS服务商可能对这些线路有限制。</p>
<p class="comment">当前节点对应的DNS线路可用线路是根据集群设置的域名获取的注意DNS服务商可能对这些线路有其他限制。</p>
</td>
</tr>
<tr>

View File

@@ -1,14 +1,16 @@
Tea.context(function () {
this.deleteCluster = function (clusterId) {
let that = this
teaweb.confirm("确定要删除此集群吗?", function () {
that.$post("/clusters/cluster/delete")
.params({
clusterId: clusterId
})
.success(function () {
window.location = "/clusters"
})
})
}
this.deleteCluster = function (clusterId) {
let that = this
teaweb.confirm("确定要删除此集群吗?", function () {
that.$post("/clusters/cluster/delete")
.params({
clusterId: clusterId
})
.success(function () {
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>{{group.name}}</td>
<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>
</td>
<td>

Some files were not shown because too many files have changed in this diff Show More