Compare commits

..

14 Commits

Author SHA1 Message Date
刘祥超
f738ba9152 缓存支持默认条件 2021-05-24 09:02:55 +08:00
刘祥超
95c27119ac 缓存策略可以根据Key前缀进行批量删除 2021-05-23 22:49:53 +08:00
刘祥超
2b2b516788 优化集群批量操作结果显示 2021-05-23 22:36:52 +08:00
刘祥超
0bb63b5bb7 SSH认证可搜索 2021-05-23 21:12:52 +08:00
刘祥超
48012072d7 SSL/TLS选择证书增加关键词搜索 2021-05-23 20:54:17 +08:00
刘祥超
0053949bc0 在服务列表中显示需要修复的错误 2021-05-23 20:44:51 +08:00
刘祥超
88e7246366 将节点日志级别筛选中的警告从warn改成warning 2021-05-23 20:03:46 +08:00
刘祥超
fbedf3b605 添加端口时校验端口范围 2021-05-23 20:03:14 +08:00
刘祥超
d5659e627d URL跳转支持正则匹配 2021-05-23 17:01:54 +08:00
刘祥超
709d4dc805 增加Licenses目录 2021-05-20 15:39:21 +08:00
刘祥超
37f052e9a5 节点监控图表流量使用秒来计算 2021-05-20 10:38:46 +08:00
刘祥超
58c765e97f 实现日志消息聚合 2021-05-19 20:52:41 +08:00
刘祥超
da332c2756 增加全局的边缘节点日志 2021-05-19 19:03:03 +08:00
刘祥超
e9d329bf8b 变更版本 2021-05-19 19:02:38 +08:00
50 changed files with 913 additions and 116 deletions

1
build/licenses/README.md Normal file
View File

@@ -0,0 +1 @@
这个目录下我们列举了所有需要公开声明的第三方License如果有遗漏烦请告知 iwind.liu@gmail.com。再次感谢这些开源软件项目和贡献人员

View File

@@ -0,0 +1,30 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
As this is fork of the official Go code the same license applies.
Extensions of the original work are copyright (c) 2011 Miek Gieben

View File

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

View File

@@ -22,7 +22,7 @@ import (
"time"
)
// RPC客户端
// RPCClient RPC客户端
type RPCClient struct {
apiConfig *configs.APIConfig
conns []*grpc.ClientConn
@@ -30,7 +30,7 @@ type RPCClient struct {
locker sync.Mutex
}
// 构造新的RPC客户端
// NewRPCClient 构造新的RPC客户端
func NewRPCClient(apiConfig *configs.APIConfig) (*RPCClient, error) {
if apiConfig == nil {
return nil, errors.New("api config should not be nil")

View File

@@ -37,7 +37,7 @@ func TestRPC_Dial_HTTP(t *testing.T) {
RPC: struct {
Endpoints []string `yaml:"endpoints"`
}{
Endpoints: []string{"127.0.0.1:8003"},
Endpoints: []string{"http://127.0.0.1:8004"},
},
NodeId: "a7e55782dab39bce0901058a1e14a0e6",
Secret: "lvyPobI3BszkJopz5nPTocOs0OLkEJ7y",
@@ -58,7 +58,7 @@ func TestRPC_Dial_HTTP_2(t *testing.T) {
RPC: struct {
Endpoints []string `yaml:"endpoints"`
}{
Endpoints: []string{"http://127.0.0.1:8003"},
Endpoints: []string{"https://127.0.0.1:8003"},
},
NodeId: "a7e55782dab39bce0901058a1e14a0e6",
Secret: "lvyPobI3BszkJopz5nPTocOs0OLkEJ7y",

View File

@@ -17,13 +17,25 @@ func (this *LogsAction) Init() {
}
func (this *LogsAction) RunGet(params struct {
NodeId int64
NodeId int64
DayFrom string
DayTo string
Keyword string
Level string
}) {
this.Data["nodeId"] = params.NodeId
this.Data["dayFrom"] = params.DayFrom
this.Data["dayTo"] = params.DayTo
this.Data["keyword"] = params.Keyword
this.Data["level"] = params.Level
countResp, err := this.RPC().NodeLogRPC().CountNodeLogs(this.AdminContext(), &pb.CountNodeLogsRequest{
Role: "node",
NodeId: params.NodeId,
Role: "node",
NodeId: params.NodeId,
DayFrom: params.DayFrom,
DayTo: params.DayTo,
Keyword: params.Keyword,
Level: params.Level,
})
if err != nil {
this.ErrorPage(err)
@@ -33,10 +45,14 @@ func (this *LogsAction) RunGet(params struct {
page := this.NewPage(count, 20)
logsResp, err := this.RPC().NodeLogRPC().ListNodeLogs(this.AdminContext(), &pb.ListNodeLogsRequest{
NodeId: params.NodeId,
Role: "node",
Offset: page.Offset,
Size: page.Size,
NodeId: params.NodeId,
Role: "node",
DayFrom: params.DayFrom,
DayTo: params.DayTo,
Keyword: params.Keyword,
Level: params.Level,
Offset: page.Offset,
Size: page.Size,
})
logs := []maps.Map{}
@@ -47,6 +63,7 @@ func (this *LogsAction) RunGet(params struct {
"createdTime": timeutil.FormatTime("Y-m-d H:i:s", log.CreatedAt),
"level": log.Level,
"isToday": timeutil.FormatTime("Y-m-d", log.CreatedAt) == timeutil.Format("Y-m-d"),
"count": log.Count,
})
}
this.Data["logs"] = logs

View File

@@ -55,8 +55,8 @@ func (this *TrafficInAction) RunPost(params struct {
if ok {
result = append(result, maps.Map{
"label": timeutil.FormatTime("H:i", timestamp),
"value": total,
"text": numberutils.FormatBytes(total),
"value": total / 60,
"text": numberutils.FormatBytes(total / 60),
})
} else {
result = append(result, maps.Map{

View File

@@ -55,8 +55,8 @@ func (this *TrafficOutAction) RunPost(params struct {
if ok {
result = append(result, maps.Map{
"label": timeutil.FormatTime("H:i", timestamp),
"value": total,
"text": numberutils.FormatBytes(total),
"value": total / 60,
"text": numberutils.FormatBytes(total / 60),
})
} else {
result = append(result, maps.Map{

View File

@@ -15,8 +15,14 @@ func (this *IndexAction) Init() {
this.Nav("", "grant", "index")
}
func (this *IndexAction) RunGet(params struct{}) {
countResp, err := this.RPC().NodeGrantRPC().CountAllEnabledNodeGrants(this.AdminContext(), &pb.CountAllEnabledNodeGrantsRequest{})
func (this *IndexAction) RunGet(params struct {
Keyword string
}) {
this.Data["keyword"] = params.Keyword
countResp, err := this.RPC().NodeGrantRPC().CountAllEnabledNodeGrants(this.AdminContext(), &pb.CountAllEnabledNodeGrantsRequest{
Keyword: params.Keyword,
})
if err != nil {
this.ErrorPage(err)
return
@@ -25,8 +31,9 @@ func (this *IndexAction) RunGet(params struct{}) {
this.Data["page"] = page.AsHTML()
grantsResp, err := this.RPC().NodeGrantRPC().ListEnabledNodeGrants(this.AdminContext(), &pb.ListEnabledNodeGrantsRequest{
Offset: page.Offset,
Size: page.Size,
Keyword: params.Keyword,
Offset: page.Offset,
Size: page.Size,
})
if err != nil {
this.ErrorPage(err)
@@ -57,6 +64,7 @@ func (this *IndexAction) RunGet(params struct{}) {
"type": grant.Method,
"name": grantutils.FindGrantMethodName(grant.Method),
},
"username": grant.Username,
"countClusters": countClusters,
"countNodes": countNodes,
})

View File

@@ -27,10 +27,12 @@ func (this *SelectPopupAction) RunGet(params struct{}) {
grantMaps := []maps.Map{}
for _, grant := range grants {
grantMaps = append(grantMaps, maps.Map{
"id": grant.Id,
"name": grant.Name,
"method": grant.Method,
"methodName": grantutils.FindGrantMethodName(grant.Method),
"id": grant.Id,
"name": grant.Name,
"method": grant.Method,
"methodName": grantutils.FindGrantMethodName(grant.Method),
"username": grant.Username,
"description": grant.Description,
})
}
this.Data["grants"] = grantMaps

View File

@@ -0,0 +1,87 @@
package logs
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 IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
}
func (this *IndexAction) RunGet(params struct {
DayFrom string
DayTo string
Keyword string
Level string
}) {
this.Data["dayFrom"] = params.DayFrom
this.Data["dayTo"] = params.DayTo
this.Data["keyword"] = params.Keyword
this.Data["level"] = params.Level
countResp, err := this.RPC().NodeLogRPC().CountNodeLogs(this.AdminContext(), &pb.CountNodeLogsRequest{
NodeId: 0,
Role: "node",
DayFrom: params.DayFrom,
DayTo: params.DayTo,
Keyword: params.Keyword,
Level: params.Level,
})
if err != nil {
this.ErrorPage(err)
return
}
count := countResp.Count
page := this.NewPage(count)
this.Data["page"] = page.AsHTML()
logsResp, err := this.RPC().NodeLogRPC().ListNodeLogs(this.AdminContext(), &pb.ListNodeLogsRequest{
NodeId: 0,
Role: "node",
DayFrom: params.DayFrom,
DayTo: params.DayTo,
Keyword: params.Keyword,
Level: params.Level,
Offset: page.Offset,
Size: page.Size,
})
logs := []maps.Map{}
for _, log := range logsResp.NodeLogs {
// 节点信息
nodeResp, err := this.RPC().NodeRPC().FindEnabledNode(this.AdminContext(), &pb.FindEnabledNodeRequest{NodeId: log.NodeId})
if err != nil {
continue
}
node := nodeResp.Node
if node == nil || node.NodeCluster == nil {
continue
}
logs = append(logs, maps.Map{
"tag": log.Tag,
"description": log.Description,
"createdTime": timeutil.FormatTime("Y-m-d H:i:s", log.CreatedAt),
"level": log.Level,
"isToday": timeutil.FormatTime("Y-m-d", log.CreatedAt) == timeutil.Format("Y-m-d"),
"count": log.Count,
"node": maps.Map{
"id": node.Id,
"cluster": maps.Map{
"id": node.NodeCluster.Id,
"name": node.NodeCluster.Name,
},
"name": node.Name,
},
})
}
this.Data["logs"] = logs
this.Show()
}

View File

@@ -0,0 +1,19 @@
package logs
import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeNode)).
Data("teaMenu", "clusters").
Data("teaSubMenu", "log").
Prefix("/clusters/logs").
Get("", new(IndexAction)).
EndAll()
})
}

View File

@@ -6,10 +6,12 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"sort"
"strconv"
"sync"
)
// MessageResult 和节点消息通讯结果定义
type MessageResult struct {
NodeId int64 `json:"nodeId"`
NodeName string `json:"nodeName"`
@@ -17,7 +19,7 @@ type MessageResult struct {
Message string `json:"message"`
}
// 向集群发送命令消息
// SendMessageToCluster 向集群发送命令消息
func SendMessageToCluster(ctx context.Context, clusterId int64, code string, msg interface{}, timeoutSeconds int32) (results []*MessageResult, err error) {
results = []*MessageResult{}
@@ -153,10 +155,17 @@ func SendMessageToCluster(ctx context.Context, clusterId int64, code string, msg
}
wg.Wait()
// 对结果进行排序
if len(results) > 0 {
sort.Slice(results, func(i, j int) bool {
return results[i].NodeId < results[j].NodeId
})
}
return
}
// 向一组节点发送命令消息
// SendMessageToNodeIds 向一组节点发送命令消息
func SendMessageToNodeIds(ctx context.Context, nodeIds []int64, code string, msg interface{}, timeoutSeconds int32) (results []*MessageResult, err error) {
results = []*MessageResult{}
if len(nodeIds) == 0 {
@@ -321,5 +330,12 @@ func SendMessageToNodeIds(ctx context.Context, nodeIds []int64, code string, msg
}
wg.Wait()
// 对结果进行排序
if len(results) > 0 {
sort.Slice(results, func(i, j int) bool {
return results[i].NodeId < results[j].NodeId
})
}
return
}

View File

@@ -13,7 +13,7 @@ import (
"time"
)
// 选择证书
// SelectPopupAction 选择证书
type SelectPopupAction struct {
actionutils.ParentAction
}
@@ -25,10 +25,12 @@ func (this *SelectPopupAction) Init() {
func (this *SelectPopupAction) RunGet(params struct {
ViewSize string
SelectedCertIds string
Keyword string
}) {
// TODO 支持关键词搜索
// TODO 列出常用和最新的证书供用户选择
this.Data["keyword"] = params.Keyword
// 已经选择的证书
selectedCertIds := []string{}
if len(params.SelectedCertIds) > 0 {
@@ -40,7 +42,9 @@ func (this *SelectPopupAction) RunGet(params struct {
}
this.Data["viewSize"] = params.ViewSize
countResp, err := this.RPC().SSLCertRPC().CountSSLCerts(this.AdminContext(), &pb.CountSSLCertRequest{})
countResp, err := this.RPC().SSLCertRPC().CountSSLCerts(this.AdminContext(), &pb.CountSSLCertRequest{
Keyword: params.Keyword,
})
if err != nil {
this.ErrorPage(err)
return
@@ -50,8 +54,9 @@ func (this *SelectPopupAction) RunGet(params struct {
this.Data["page"] = page.AsHTML()
listResp, err := this.RPC().SSLCertRPC().ListSSLCerts(this.AdminContext(), &pb.ListSSLCertsRequest{
Offset: page.Offset,
Size: page.Size,
Keyword: params.Keyword,
Offset: page.Offset,
Size: page.Size,
})
certConfigs := []*sslconfigs.SSLCertConfig{}

View File

@@ -12,7 +12,7 @@ import (
"github.com/iwind/TeaGo/types"
"net/http"
"strconv"
strings "strings"
"strings"
)
type PurgeAction struct {
@@ -53,6 +53,7 @@ func (this *PurgeAction) RunGet(params struct {
func (this *PurgeAction) RunPost(params struct {
CachePolicyId int64
ClusterId int64
Type string
Keys string
Must *actions.Must
}) {
@@ -96,6 +97,11 @@ func (this *PurgeAction) RunPost(params struct {
CachePolicyJSON: cachePolicyJSON,
Keys: realKeys,
}
if params.Type == "prefix" {
msg.Type = messageconfigs.PurgeCacheMessageTypeDir
} else {
msg.Type = messageconfigs.PurgeCacheMessageTypeFile
}
results, err := nodeutils.SendMessageToCluster(this.AdminContext(), params.ClusterId, messageconfigs.MessageCodePurgeCache, msg, 10)
if err != nil {
this.ErrorPage(err)

View File

@@ -63,8 +63,13 @@ func (this *UpdateAction) RunPost(params struct {
Description string
IsOn bool
RefsJSON []byte
Must *actions.Must
}) {
// 创建日志
defer this.CreateLog(oplogs.LevelInfo, "修改缓存策略:%d", params.CachePolicyId)
params.Must.
Field("name", params.Name).
Require("请输入策略名称")
@@ -104,6 +109,22 @@ func (this *UpdateAction) RunPost(params struct {
this.ErrorPage(err)
return
}
// 校验缓存条件
refs := []*serverconfigs.HTTPCacheRef{}
if len(params.RefsJSON) > 0 {
err = json.Unmarshal(params.RefsJSON, &refs)
if err != nil {
this.Fail("缓存条件解析失败:" + err.Error())
}
for _, ref := range refs {
err = ref.Init()
if err != nil {
this.Fail("缓存条件校验失败:" + err.Error())
}
}
}
_, err = this.RPC().HTTPCachePolicyRPC().UpdateHTTPCachePolicy(this.AdminContext(), &pb.UpdateHTTPCachePolicyRequest{
HttpCachePolicyId: params.CachePolicyId,
IsOn: params.IsOn,
@@ -120,8 +141,15 @@ func (this *UpdateAction) RunPost(params struct {
return
}
// 创建日志
defer this.CreateLog(oplogs.LevelInfo, "修改缓存策略:%d", params.CachePolicyId)
// 修改缓存条件
_, err = this.RPC().HTTPCachePolicyRPC().UpdateHTTPCachePolicyRefs(this.AdminContext(), &pb.UpdateHTTPCachePolicyRefsRequest{
HttpCachePolicyId: params.CachePolicyId,
RefsJSON: params.RefsJSON,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,24 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package servers
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type FixLogAction struct {
actionutils.ParentAction
}
func (this *FixLogAction) RunPost(params struct {
LogId int64
}) {
_, err := this.RPC().NodeLogRPC().FixNodeLog(this.AdminContext(), &pb.FixNodeLogRequest{NodeLogId: params.LogId})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -4,9 +4,11 @@ import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
"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 {
@@ -250,5 +252,48 @@ func (this *IndexAction) RunGet(params struct {
// 是否有用户管理权限
this.Data["canVisitUser"] = configloaders.AllowModule(this.AdminId(), configloaders.AdminModuleCodeUser)
// 显示服务相关的日志
errorLogsResp, err := this.RPC().NodeLogRPC().ListNodeLogs(this.AdminContext(), &pb.ListNodeLogsRequest{
NodeId: 0,
Role: "node",
Offset: 0,
Size: 10,
Level: "error",
FixedState: int32(configutils.BoolStateNo),
AllServers: true,
})
if err != nil {
this.ErrorPage(err)
return
}
errorLogMaps := []maps.Map{}
for _, errorLog := range errorLogsResp.NodeLogs {
serverResp, err := this.RPC().ServerRPC().FindEnabledUserServerBasic(this.AdminContext(), &pb.FindEnabledUserServerBasicRequest{ServerId: errorLog.ServerId})
if err != nil {
this.ErrorPage(err)
return
}
server := serverResp.Server
if server == nil {
// 设置为已修复
_, err = this.RPC().NodeLogRPC().FixNodeLog(this.AdminContext(), &pb.FixNodeLogRequest{NodeLogId: errorLog.Id})
if err != nil {
this.ErrorPage(err)
return
}
continue
}
errorLogMaps = append(errorLogMaps, maps.Map{
"id": errorLog.Id,
"description": errorLog.Description,
"createdTime": timeutil.FormatTime("Y-m-d H:i:s", errorLog.CreatedAt),
"serverId": errorLog.ServerId,
"serverName": server.Name,
})
}
this.Data["errorLogs"] = errorLogMaps
this.Show()
}

View File

@@ -15,6 +15,7 @@ func init() {
Get("", new(IndexAction)).
GetPost("/create", new(CreateAction)).
GetPost("/update", new(UpdateAction)).
Post("/fixLog", new(FixLogAction)).
GetPost("/addPortPopup", new(AddPortPopupAction)).
GetPost("/addServerNamePopup", new(AddServerNamePopupAction)).

View File

@@ -10,6 +10,8 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"regexp"
)
type IndexAction struct {
@@ -76,6 +78,24 @@ func (this *IndexAction) RunPost(params struct {
this.Fail("端口地址解析失败:" + err.Error())
}
// 检查端口地址是否正确
for _, addr := range addresses {
err = addr.Init()
if err != nil {
this.Fail("绑定端口校验失败:" + err.Error())
}
if regexp.MustCompile(`^\d+$`).MatchString(addr.PortRange) {
port := types.Int(addr.PortRange)
if port > 65535 {
this.Fail("绑定的端口地址不能大于65535")
}
if port == 443 {
this.Fail("端口443通常是HTTPS的端口不能用在HTTP上")
}
}
}
server, _, isOk := serverutils.FindServer(this.Parent(), params.ServerId)
if !isOk {
return

View File

@@ -12,6 +12,7 @@ import (
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"regexp"
)
type IndexAction struct {
@@ -87,7 +88,23 @@ func (this *IndexAction) RunPost(params struct {
this.Fail("端口地址解析失败:" + err.Error())
}
// TODO 校验addresses
// 检查端口地址是否正确
for _, addr := range addresses {
err = addr.Init()
if err != nil {
this.Fail("绑定端口校验失败:" + err.Error())
}
if regexp.MustCompile(`^\d+$`).MatchString(addr.PortRange) {
port := types.Int(addr.PortRange)
if port > 65535 {
this.Fail("绑定的端口地址不能大于65535")
}
if port == 80 {
this.Fail("端口80通常是HTTP的端口不能用在HTTPS上")
}
}
}
// 校验SSL
var sslPolicyId = int64(0)

View File

@@ -6,6 +6,7 @@ import (
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"net/url"
"regexp"
)
type CreatePopupAction struct {
@@ -27,6 +28,7 @@ func (this *CreatePopupAction) RunPost(params struct {
BeforeURL string
AfterURL string
MatchPrefix bool
MatchRegexp bool
KeepRequestURI bool
Status int
@@ -39,7 +41,12 @@ func (this *CreatePopupAction) RunPost(params struct {
Require("请填写跳转前的URL")
// 校验格式
{
if params.MatchRegexp {
_, err := regexp.Compile(params.BeforeURL)
if err != nil {
this.Fail("跳转前URL正则表达式错误" + err.Error())
}
} else {
u, err := url.Parse(params.BeforeURL)
if err != nil {
this.FailField("beforeURL", "请输入正确的跳转前URL")
@@ -56,7 +63,9 @@ func (this *CreatePopupAction) RunPost(params struct {
Require("请填写跳转后URL")
// 校验格式
{
if params.MatchRegexp {
// 正则表达式情况下不做校验
} else {
u, err := url.Parse(params.AfterURL)
if err != nil {
this.FailField("afterURL", "请输入正确的跳转后URL")
@@ -76,6 +85,7 @@ func (this *CreatePopupAction) RunPost(params struct {
"beforeURL": params.BeforeURL,
"afterURL": params.AfterURL,
"matchPrefix": params.MatchPrefix,
"matchRegexp": params.MatchRegexp,
"keepRequestURI": params.KeepRequestURI,
"isOn": true,
}

View File

@@ -181,6 +181,11 @@ func (this *userMustAuth) modules(adminId int64) []maps.Map {
"subtitle": "集群列表",
"icon": "cloud",
"subItems": []maps.Map{
{
"name": "节点日志",
"url": "/clusters/logs",
"code": "log",
},
{
"name": "SSH认证",
"url": "/clusters/grants",

View File

@@ -15,6 +15,7 @@ import (
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/grants"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/logs"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/regions"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/regions/items"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/tasks"

View File

@@ -0,0 +1,58 @@
Vue.component("http-cache-refs-box", {
props: ["v-cache-refs"],
data: function () {
let refs = this.vCacheRefs
if (refs == null) {
refs = []
}
return {
refs: refs
}
},
methods: {
timeUnitName: function (unit) {
switch (unit) {
case "ms":
return "毫秒"
case "second":
return "秒"
case "minute":
return "分钟"
case "hour":
return "小时"
case "day":
return "天"
case "week":
return "周 "
}
return unit
}
},
template: `<div>
<input type="hidden" name="refsJSON" :value="JSON.stringify(refs)"/>
<p class="comment" v-if="refs.length == 0">暂时还没有缓存条件。</p>
<div v-show="refs.length > 0">
<table class="ui table selectable celled">
<thead>
<tr>
<th>缓存条件</th>
<th class="width10">缓存时间</th>
<th class="two op">操作</th>
</tr>
<tr v-for="(cacheRef, index) in refs">
<td>
<http-request-conds-view :v-conds="cacheRef.conds"></http-request-conds-view>
</td>
<td>{{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}}</td>
<td>
<a href="" @click.prevent="updateRef(index, cacheRef)">修改</a> &nbsp;
<a href="" @click.prevent="removeRef(index)">删除</a>
</td>
</tr>
</thead>
</table>
</div>
<div class="margin"></div>
</div>`
})

View File

@@ -0,0 +1,105 @@
Vue.component("http-cache-refs-config-box", {
props: ["v-cache-refs"],
data: function () {
let refs = this.vCacheRefs
if (refs == null) {
refs = []
}
return {
refs: refs
}
},
methods: {
addRef: function () {
window.UPDATING_CACHE_REF = null
let width = window.innerWidth
if (width > 1024) {
width = 1024
}
let height = window.innerHeight
if (height > 500) {
height = 500
}
let that = this
teaweb.popup("/servers/server/settings/cache/createPopup", {
width: width + "px",
height: height + "px",
callback: function (resp) {
that.refs.push(resp.data.cacheRef)
}
})
},
updateRef: function (index, cacheRef) {
window.UPDATING_CACHE_REF = cacheRef
let width = window.innerWidth
if (width > 1024) {
width = 1024
}
let height = window.innerHeight
if (height > 500) {
height = 500
}
let that = this
teaweb.popup("/servers/server/settings/cache/createPopup", {
width: width + "px",
height: height + "px",
callback: function (resp) {
Vue.set(that.refs, index, resp.data.cacheRef)
}
})
},
removeRef: function (index) {
let that = this
teaweb.confirm("确定要删除此缓存设置吗?", function () {
that.refs.$remove(index)
})
},
timeUnitName: function (unit) {
switch (unit) {
case "ms":
return "毫秒"
case "second":
return "秒"
case "minute":
return "分钟"
case "hour":
return "小时"
case "day":
return "天"
case "week":
return "周 "
}
return unit
}
},
template: `<div>
<input type="hidden" name="refsJSON" :value="JSON.stringify(refs)"/>
<div>
<table class="ui table selectable celled" v-show="refs.length > 0">
<thead>
<tr>
<th>缓存条件</th>
<th class="width10">缓存时间</th>
<th class="two op">操作</th>
</tr>
<tr v-for="(cacheRef, index) in refs">
<td>
<http-request-conds-view :v-conds="cacheRef.conds"></http-request-conds-view>
</td>
<td>{{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}}</td>
<td>
<a href="" @click.prevent="updateRef(index, cacheRef)">修改</a> &nbsp;
<a href="" @click.prevent="removeRef(index)">删除</a>
</td>
</tr>
</thead>
</table>
<button class="ui button tiny" @click.prevent="addRef">+添加缓存设置</button>
</div>
<div class="margin"></div>
</div>`
})

View File

@@ -50,7 +50,10 @@ Vue.component("http-host-redirect-box", {
<!-- TODO 将来支持排序并支持isOn切换 -->
<div v-if="redirects.length > 0">
<div v-for="(redirect, index) in redirects" class="ui label basic small" style="margin-bottom: 0.5em;margin-top: 0.5em">
<span v-if="redirect.status > 0">[{{redirect.status}}]</span><span v-if="redirect.matchPrefix">[prefix]</span> {{redirect.beforeURL}} -&gt; {{redirect.afterURL}} <a href="" @click.prevent="update(index, redirect)" title="修改"><i class="icon pencil small"></i></a> &nbsp; <a href="" @click.prevent="remove(index)" title="删除"><i class="icon remove"></i></a>
<span v-if="redirect.status > 0" class="small grey">[{{redirect.status}}]</span>
<span v-if="redirect.matchPrefix" class="small grey">[匹配前缀]</span>
<span v-if="redirect.matchRegexp" class="small grey">[正则匹配]</span>
{{redirect.beforeURL}} -&gt; {{redirect.afterURL}} <a href="" @click.prevent="update(index, redirect)" title="修改"><i class="icon pencil small"></i></a> &nbsp; <a href="" @click.prevent="remove(index)" title="删除"><i class="icon remove"></i></a>
</div>
<div class="ui divider"></div>
</div>

View File

@@ -11,7 +11,9 @@ Vue.component("origin-list-box", {
teaweb.popup("/servers/server/settings/origins/addPopup?originType=primary&" + this.vParams, {
height: "24em",
callback: function (resp) {
window.location.reload()
teaweb.success("保存成功", function () {
window.location.reload()
})
}
})
},
@@ -19,7 +21,9 @@ Vue.component("origin-list-box", {
teaweb.popup("/servers/server/settings/origins/addPopup?originType=backup&" + this.vParams, {
height: "24em",
callback: function (resp) {
window.location.reload()
teaweb.success("保存成功", function () {
window.location.reload()
})
}
})
},
@@ -27,7 +31,9 @@ Vue.component("origin-list-box", {
teaweb.popup("/servers/server/settings/origins/updatePopup?originType=" + originType + "&" + this.vParams + "&originId=" + originId, {
height: "24em",
callback: function (resp) {
window.location.reload()
teaweb.success("保存成功", function () {
window.location.reload()
})
}
})
},
@@ -37,7 +43,9 @@ Vue.component("origin-list-box", {
Tea.action("/servers/server/settings/origins/delete?" + that.vParams + "&originId=" + originId + "&originType=" + originType)
.post()
.success(function () {
window.location.reload()
teaweb.success("保存成功", function () {
window.location.reload()
})
})
})
}

View File

@@ -1,20 +1,58 @@
{$layout}
{$template "node_menu"}
{$template "node_menu"}
{$var "header"}
<!-- datepicker -->
<script type="text/javascript" src="/js/moment.min.js"></script>
<script type="text/javascript" src="/js/pikaday.js"></script>
<link rel="stylesheet" href="/js/pikaday.css"/>
<link rel="stylesheet" href="/js/pikaday.theme.css"/>
<link rel="stylesheet" href="/js/pikaday.triangle.css"/>
{$end}
<p class="comment" v-if="logs.length == 0">暂时还没有日志。</p>
<form method="get" action="/clusters/cluster/node/logs" class="ui form" autocomplete="off">
<input type="hidden" name="clusterId" :value="clusterId"/>
<input type="hidden" name="nodeId" :value="nodeId"/>
<div class="ui fields inline">
<div class="ui field">
<input type="text" name="dayFrom" placeholder="开始日期" v-model="dayFrom" value="" style="width:8em" id="day-from-picker"/>
</div>
<div class="ui field">
<input type="text" name="dayTo" placeholder="结束日期" v-model="dayTo" value="" style="width:8em" id="day-to-picker"/>
</div>
<div class="ui field">
<select class="ui dropdown" name="level" v-model="level">
<option value="">[级别]</option>
<option value="error">错误</option>
<option value="warning">警告</option>
<option value="info">信息</option>
</select>
</div>
<div class="ui field">
<input type="text" name="keyword" style="width:10em" v-model="keyword" placeholder="关键词"/>
</div>
<div class="ui field">
<button type="submit" class="ui button">查询</button>
</div>
<div class="ui field" v-if="dayFrom.length > 0 || dayTo.length > 0 || keyword.length > 0 || level.length > 0">
<a :href="'/clusters/cluster/node/logs?clusterId=' + clusterId + '&nodeId=' + nodeId">[清除条件]</a>
</div>
</div>
</form>
<table class="ui table selectable" v-if="logs.length > 0">
<thead>
<tr>
<p class="comment" v-if="logs.length == 0">暂时还没有日志。</p>
</tr>
</thead>
<tr v-for="log in logs">
<td>
<pre class="log-box"><span :class="{red:log.level == 'error', yellow:log.level == 'warning'}"><span v-if="!log.isToday">[{{log.createdTime}}]</span><strong v-if="log.isToday">[{{log.createdTime}}]</strong>[{{log.tag}}]{{log.description}}</span></pre>
</td>
</tr>
</table>
<table class="ui table selectable" v-if="logs.length > 0">
<thead>
<tr>
<div class="page" v-html="page"></div>
</tr>
</thead>
<tr v-for="log in logs">
<td>
<pre class="log-box"><span :class="{red:log.level == 'error', yellow:log.level == 'warning'}"><span v-if="!log.isToday">[{{log.createdTime}}]</span><strong v-if="log.isToday">[{{log.createdTime}}]</strong>[{{log.tag}}]{{log.description}}</span> &nbsp; <span v-if="log.count > 0" class="ui label tiny red">共{{log.count}}条</span></pre>
</td>
</tr>
</table>
<div class="page" v-html="page"></div>

View File

@@ -0,0 +1,6 @@
Tea.context(function () {
this.$delay(function () {
teaweb.datepicker("day-from-picker")
teaweb.datepicker("day-to-picker")
})
})

View File

@@ -1,6 +1,21 @@
{$layout}
{$template "menu"}
<!-- 搜索表单 -->
<form class="ui form" method="get" action="/clusters/grants">
<div class="margin"></div>
<div class="ui fields inline">
<div class="ui field">
<input type="text" name="keyword" placeholder="名称、用户名等..." v-model="keyword"/>
</div>
<div class="ui field">
<button class="ui button" type="submit">搜索</button>
</div>
</div>
</form>
<!-- SSH认证列表 -->
<div class="ui message" v-if="grants.length == 0">暂时还没有认证信息。</div>
<table class="ui table selectable celled" v-if="grants.length > 0">
@@ -8,6 +23,7 @@
<tr>
<th>名称</th>
<th>类型</th>
<th>用户名</th>
<th class="center width5">集群数</th>
<th class="center width5">节点数</th>
<th class="two op">操作</th>
@@ -18,6 +34,10 @@
<td>
<span class="ui label tiny basic">{{grant.method.name}}</span>
</td>
<td>
<span v-if="grant.username.length > 0">{{grant.username}}</span>
<span v-else class="disabled">-</span>
</td>
<td class="center">
<span v-if="grant.countClusters > 0">{{grant.countClusters}}</span>
<span v-else class="disabled">0</span>

View File

@@ -2,11 +2,19 @@
<h3>选择认证</h3>
<table class="ui table definition">
<form class="ui form">
<div class="ui fields inline">
<div class="ui field">
<input type="text" placeholder="搜索名称、用户名等" v-model="keyword"/>
</div>
</div>
</form>
<table class="ui table">
<tr>
<td>
<span v-if="grants.length == 0">暂时还没有可用的认证。</span>
<a class="ui label small basic" v-for="grant in grants" :class="{blue:grantId == grant.id}" @click.prevent="selectGrant(grant)" style="margin-bottom:0.5em">{{grant.name}} <span class="small">{{grant.methodName}}</span></a>
<a class="ui label small basic" v-for="grant in grants" :class="{blue:grantId == grant.id}" @click.prevent="selectGrant(grant)" style="margin-bottom:0.5em">{{grant.name}} <span class="small">{{grant.methodName}}</span><span v-if="grant.username.length > 0" class="small">{{grant.username}}</span></a>
<p class="comment">请点击选中某个认证。</p>
</td>
</tr>

View File

@@ -1,5 +1,7 @@
Tea.context(function () {
this.grantId = 0;
this.grantId = 0
this.keyword = ""
let allGrants = this.grants.$copy()
this.selectGrant = function (grant) {
NotifyPopup({
@@ -8,5 +10,20 @@ Tea.context(function () {
grant: grant
}
})
};
});
}
this.$delay(function () {
let that = this
this.$watch("keyword", function (keyword) {
if (keyword.length > 0) {
that.grants = allGrants.$findAll(function (k, grant) {
return teaweb.match(grant.name, keyword)
|| teaweb.match(grant.description, keyword)
|| teaweb.match(grant.username, keyword)
})
} else {
that.grants = allGrants
}
})
})
})

View File

@@ -0,0 +1,5 @@
pre.log-box {
margin: 0;
padding: 0;
}
/*# sourceMappingURL=index.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["index.less"],"names":[],"mappings":"AAAA,GAAG;EACF,SAAA;EACA,UAAA","file":"index.css"}

View File

@@ -0,0 +1,62 @@
{$layout}
{$var "header"}
<!-- datepicker -->
<script type="text/javascript" src="/js/moment.min.js"></script>
<script type="text/javascript" src="/js/pikaday.js"></script>
<link rel="stylesheet" href="/js/pikaday.css"/>
<link rel="stylesheet" href="/js/pikaday.theme.css"/>
<link rel="stylesheet" href="/js/pikaday.triangle.css"/>
{$end}
<div class="margin"></div>
<form method="get" action="/clusters/logs" class="ui form" autocomplete="off">
<div class="ui fields inline">
<div class="ui field">
<input type="text" name="dayFrom" placeholder="开始日期" v-model="dayFrom" value="" style="width:8em" id="day-from-picker"/>
</div>
<div class="ui field">
<input type="text" name="dayTo" placeholder="结束日期" v-model="dayTo" value="" style="width:8em" id="day-to-picker"/>
</div>
<div class="ui field">
<select class="ui dropdown" name="level" v-model="level">
<option value="">[级别]</option>
<option value="error">错误</option>
<option value="warning">警告</option>
<option value="info">信息</option>
</select>
</div>
<div class="ui field">
<input type="text" name="keyword" style="width:10em" v-model="keyword" placeholder="关键词"/>
</div>
<div class="ui field">
<button type="submit" class="ui button">查询</button>
</div>
<div class="ui field" v-if="dayFrom.length > 0 || dayTo.length > 0 || keyword.length > 0 || level.length > 0">
<a href="/clusters/logs">[清除条件]</a>
</div>
</div>
</form>
<p class="comment" v-if="logs.length == 0">暂时还没有日志。</p>
<table class="ui table selectable celled" v-if="logs.length > 0">
<thead>
<tr>
<th>集群</th>
<th>节点</th>
<th>信息</th>
</tr>
</thead>
<tr v-for="log in logs">
<td nowrap=""><link-icon :href="'/clusters/cluster?clusterId=' + log.node.cluster.id">{{log.node.cluster.name}}</link-icon></td>
<td nowrap=""><link-icon :href="'/clusters/cluster/node?clusterId=' + log.node.cluster.id + '&nodeId=' + log.node.id">{{log.node.name}}</link-icon></td>
<td>
<pre class="log-box"><span :class="{red:log.level == 'error', yellow:log.level == 'warning'}"><span v-if="!log.isToday">[{{log.createdTime}}]</span><strong v-if="log.isToday">[{{log.createdTime}}]</strong>[{{log.tag}}]{{log.description}}</span> &nbsp; <span v-if="log.count > 0" class="ui label tiny red">共{{log.count}}条</span></pre>
</td>
</tr>
</table>
<div class="page" v-html="page"></div>

View File

@@ -0,0 +1,6 @@
Tea.context(function () {
this.$delay(function () {
teaweb.datepicker("day-from-picker")
teaweb.datepicker("day-to-picker")
})
})

View File

@@ -0,0 +1,4 @@
pre.log-box {
margin: 0;
padding: 0;
}

View File

@@ -1,6 +1,20 @@
{$layout "layout_popup"}
<h3>选择证书</h3>
<!-- 搜索表单 -->
<form class="ui form" action="/servers/certs/selectPopup">
<div class="ui fields inline">
<div class="ui field">
<input type="text" name="keyword" v-model="keyword" placeholder="域名、说明文字等"/>
</div>
<div class="ui field">
<button class="ui button" type="submit">搜索</button>
</div>
</div>
</form>
<!-- 证书列表 -->
<p class="comment" v-if="certs.length == 0">暂时还没有相关的证书。</p>
<table class="ui table selectable celled" v-if="certs.length > 0">
<thead>

View File

@@ -80,6 +80,12 @@
</tbody>
</table>
<!-- 默认缓存条件 -->
<h4>默认缓存条件</h4>
<http-cache-refs-box :v-cache-refs="cachePolicy.cacheRefs"></http-cache-refs-box>
<!-- 使用此策略的集群 -->
<h4>使用此策略的集群</h4>
<p class="comment" v-if="clusters.length == 0">暂时还没有集群使用此策略。</p>
<table class="ui table selectable" v-if="clusters.length > 0">

View File

@@ -1,37 +1,47 @@
{$layout}
{$template "policy_menu"}
{$template "policy_menu"}
<h3>选择集群</h3>
<select class="ui dropdown auto-width" v-model="clusterId">
<option v-for="cluster in clusters" :value="cluster.id">{{cluster.name}}</option>
</select>
<div class="ui divider"></div>
<h3>选择集群</h3>
<select class="ui dropdown auto-width" v-model="clusterId">
<option v-for="cluster in clusters" :value="cluster.id">{{cluster.name}}</option>
</select>
<div class="ui divider"></div>
<h3>批量删除</h3>
<p class="comment">可以批量删除一组Key。</p>
<form method="post" class="ui form" data-tea-action="$" data-tea-before="before" data-tea-success="success" data-tea-fail="fail" data-tea-done="done">
<input type="hidden" name="cachePolicyId" :value="cachePolicyId"/>
<input type="hidden" name="clusterId" :value="clusterId"/>
<table class="ui table definition selectable">
<tr>
<td>Key列表</td>
<td>
<textarea name="keys" rows="10" ref="focus"></textarea>
<p class="comment">每行一个Key。</p>
</td>
</tr>
<tr>
<td class="title">操作结果</td>
<td>
<div v-if="isRequesting">数据发送中...</div>
<span class="red" v-if="!isRequesting && !isOk && message.length > 0">失败:{{message}}</span>
<div v-if="!isRequesting && isOk">
<span v-if="results.length == 0" class="red">此集群下没有任何可用的节点。</span>
<div class="ui label tiny" v-for="one in results" :class="{green:one.isOk, red:!one.isOk}" style="margin-bottom: 0.5em">{{one.nodeName}}{{one.message}}</div>
</div>
</td>
</tr>
</table>
<submit-btn v-if="!isRequesting">提交</submit-btn>
</form>
<h3>批量删除</h3>
<p class="comment">可以批量删除一组Key。</p>
<form method="post" class="ui form" data-tea-action="$" data-tea-before="before" data-tea-success="success" data-tea-fail="fail" data-tea-done="done">
<input type="hidden" name="cachePolicyId" :value="cachePolicyId"/>
<input type="hidden" name="clusterId" :value="clusterId"/>
<table class="ui table definition selectable">
<tr>
<td class="title">操作类型</td>
<td>
<radio name="type" :v-value="'key'" v-model="type">根据Key</radio> &nbsp;
<radio name="type" :v-value="'prefix'" v-model="type">根据前缀</radio>
</td>
</tr>
<tr>
<td>
<span v-if="type == 'key'">Key列表</span>
<span v-if="type == 'prefix'">Key前缀列表</span>
</td>
<td>
<textarea name="keys" rows="10" ref="focus"></textarea>
<p class="comment" v-if="type == 'key'">每行一个Key比如是一个完整的URL<code-label>https://example.com/hello/world.html</code-label></p>
<p class="comment" v-if="type == 'prefix'">每行一个Key前缀比如是一个URL前缀<code-label>https://example.com/hello/</code-label></p>
</td>
</tr>
<tr>
<td>操作结果</td>
<td>
<div v-if="isRequesting">数据发送中...</div>
<span class="red" v-if="!isRequesting && !isOk && message.length > 0">失败:{{message}}</span>
<div v-if="!isRequesting && isOk">
<span v-if="results.length == 0" class="red">此集群下没有任何可用的节点。</span>
<div class="ui label tiny" v-for="one in results" :class="{green:one.isOk, red:!one.isOk}" style="margin-bottom: 0.5em">{{one.nodeName}}{{one.message}}</div>
</div>
</td>
</tr>
</table>
<submit-btn v-if="!isRequesting">提交</submit-btn>
</form>

View File

@@ -31,4 +31,9 @@ Tea.context(function () {
this.done = function () {
this.isRequesting = false
}
});
/**
* 操作类型
*/
this.type = "key" // key | prefix
})

View File

@@ -94,5 +94,9 @@
</tr>
</tbody>
</table>
<h4>默认缓存条件</h4>
<http-cache-refs-config-box :v-cache-refs="cachePolicy.cacheRefs"></http-cache-refs-config-box>
<submit-btn></submit-btn>
</form>

View File

@@ -0,0 +1,6 @@
.ui.message .icon {
position: absolute;
right: 1em;
top: 1.8em;
}
/*# sourceMappingURL=index.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["index.less"],"names":[],"mappings":"AAAA,GAAG,QACF;EACC,kBAAA;EACA,UAAA;EACA,UAAA","file":"index.css"}

View File

@@ -1,6 +1,23 @@
{$layout}
{$template "menu"}
<!-- 错误日志 -->
<div v-if="errorLogs.length > 0">
<div class="margin"></div>
<div class="ui menu tabular attached">
<span class="ui item active"><span class="red">需要修复的错误</span></span>
</div>
<div class="ui segment attached">
<div class="ui message error" v-for="log in errorLogs">
[{{log.createdTime}}]
<a :href="'/servers/server/settings?serverId=' + log.serverId"><span v-if="log.serverName.length > 0">[{{log.serverName}}]</span><span v-else>[服务]</span></a>
{{log.description}}
<a href="" title="关闭" @click.prevent="fixLog(log.id)"><i class="ui icon remove small"></i></a>
</div>
</div>
</div>
<!-- 搜索表单 -->
<form method="get" class="ui form" action="/servers">
<input type="hidden" name="auditingFlag" :value="auditingFlag"/>
<div class="ui margin"></div>

View File

@@ -46,4 +46,20 @@ Tea.context(function () {
this.showLatest = function () {
this.latestVisible = !this.latestVisible
}
/**
* 错误日志相关
*/
this.fixLog = function (logId) {
let that = this
teaweb.confirm("确定要关闭此错误提示吗?", function () {
that.$post(".fixLog")
.params({
logId: logId
})
.success(function () {
teaweb.reload()
})
})
}
})

View File

@@ -0,0 +1,7 @@
.ui.message {
.icon {
position: absolute;
right: 1em;
top: 1.8em;
}
}

View File

@@ -5,6 +5,8 @@
<form class="ui form" data-tea-success="success" data-tea-action="$">
<csrf-token></csrf-token>
<input type="hidden" name="matchPrefix" :value="redirect.matchPrefix ? 1 : 0"/>
<input type="hidden" name="matchRegexp" :value="redirect.matchRegexp ? 1 : 0"/>
<table class="ui table definition selectable">
<tr>
@@ -17,15 +19,17 @@
<tr>
<td>匹配模式</td>
<td>
<select class="ui dropdown auto-width" name="matchPrefix" v-model="matchPrefix">
<option value="0">精准匹配</option>
<option value="1">匹配前缀</option>
<select class="ui dropdown auto-width" name="mode" v-model="mode">
<option value="equal">精准匹配</option>
<option value="matchPrefix">匹配前缀</option>
<option value="matchRegexp">正则匹配</option>
</select>
<p class="comment" v-if="matchPrefix == 0">精准匹配跳转前的URL。</p>
<p class="comment" v-if="matchPrefix == 1">只要URL头部部分包含跳转前URL即可跳转。</p>
<p class="comment" v-if="mode == 'equal'">精准匹配跳转前的URL即只有访问完全一样的URL才会跳转</p>
<p class="comment" v-if="mode == 'matchPrefix'">只要访问的URL头部部分包含跳转前URL即可跳转。</p>
<p class="comment" v-if="mode == 'matchRegexp'">可以在跳转前URL中使用正则表达式然后可以在跳转后URL中使用正则表达式中括号的变量比如<code-label>${1}</code-label><code-label>${2}</code-label>等。</p>
</td>
</tr>
<tr v-if="matchPrefix == 1">
<tr v-if="mode == 'matchPrefix'">
<td>是否保留URL路径参数</td>
<td>
<checkbox name="keepRequestURI" value="1" v-model="redirect.keepRequestURI"></checkbox>

View File

@@ -1,17 +1,41 @@
Tea.context(function () {
this.isCreating = true
if (window.parent.UPDATING_REDIRECT != null) {
this.isCreating = false
this.redirect = window.parent.UPDATING_REDIRECT
} else {
this.redirect = {
status: 0,
beforeURL: "",
afterURL: "",
matchPrefix: false,
keepRequestURI: false
}
}
this.isCreating = true
if (window.parent.UPDATING_REDIRECT != null) {
this.isCreating = false
this.redirect = window.parent.UPDATING_REDIRECT
} else {
this.redirect = {
status: 0,
beforeURL: "",
afterURL: "",
matchPrefix: false,
matchRegexp: false,
keepRequestURI: false
}
}
this.matchPrefix = (this.redirect.matchPrefix ? 1 : 0)
this.mode = ""
if (this.redirect.matchPrefix) {
this.mode = "matchPrefix"
} else if (this.redirect.matchRegexp) {
this.mode = "matchRegexp"
} else {
this.mode = "equal"
}
this.$delay(function () {
let that = this
this.$watch("mode", function (v) {
if (v == "matchPrefix") {
that.redirect.matchPrefix = true
that.redirect.matchRegexp = false
} else if (v == "matchRegexp") {
that.redirect.matchPrefix = false
that.redirect.matchRegexp = true
} else {
that.redirect.matchPrefix = false
that.redirect.matchRegexp = false
}
})
})
})