前端页面

This commit is contained in:
robin
2026-02-24 19:10:27 +08:00
parent 60dc87e0f2
commit 2eb32b9f1f
59 changed files with 1537 additions and 890 deletions

View File

@@ -0,0 +1,123 @@
package httpdns
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"regexp"
"strings"
)
type AddPortPopupAction struct {
actionutils.ParentAction
}
func (this *AddPortPopupAction) Init() {
this.Nav("", "", "")
}
func (this *AddPortPopupAction) RunGet(params struct {
Protocol string
From string
SupportRange bool
}) {
this.Data["from"] = params.From
var protocols = serverconfigs.FindAllServerProtocols()
if len(params.Protocol) > 0 {
result := []maps.Map{}
for _, p := range protocols {
if p.GetString("code") == params.Protocol {
result = append(result, p)
}
}
protocols = result
}
this.Data["protocols"] = protocols
this.Data["supportRange"] = params.SupportRange
this.Show()
}
func (this *AddPortPopupAction) RunPost(params struct {
SupportRange bool
Protocol string
Address string
Must *actions.Must
}) {
// 校验地址
var addr = maps.Map{
"protocol": params.Protocol,
"host": "",
"portRange": "",
"minPort": 0,
"maxPort": 0,
}
var portRegexp = regexp.MustCompile(`^\d+$`)
if portRegexp.MatchString(params.Address) { // 单个端口
addr["portRange"] = this.checkPort(params.Address)
} else if params.SupportRange && regexp.MustCompile(`^\d+\s*-\s*\d+$`).MatchString(params.Address) { // Port1-Port2
addr["portRange"], addr["minPort"], addr["maxPort"] = this.checkPortRange(params.Address)
} else if strings.Contains(params.Address, ":") { // IP:Port
index := strings.LastIndex(params.Address, ":")
addr["host"] = strings.TrimSpace(params.Address[:index])
port := strings.TrimSpace(params.Address[index+1:])
if portRegexp.MatchString(port) {
addr["portRange"] = this.checkPort(port)
} else if params.SupportRange && regexp.MustCompile(`^\d+\s*-\s*\d+$`).MatchString(port) { // Port1-Port2
addr["portRange"], addr["minPort"], addr["maxPort"] = this.checkPortRange(port)
} else {
this.FailField("address", "请输入正确的端口或者网络地址")
}
} else {
this.FailField("address", "请输入正确的端口或者网络地址")
}
this.Data["address"] = addr
this.Success()
}
func (this *AddPortPopupAction) checkPort(port string) (portRange string) {
var intPort = types.Int(port)
if intPort < 1 {
this.FailField("address", "端口号不能小于1")
}
if intPort > 65535 {
this.FailField("address", "端口号不能大于65535")
}
return port
}
func (this *AddPortPopupAction) checkPortRange(port string) (portRange string, minPort int, maxPort int) {
var pieces = strings.Split(port, "-")
var piece1 = strings.TrimSpace(pieces[0])
var piece2 = strings.TrimSpace(pieces[1])
var port1 = types.Int(piece1)
var port2 = types.Int(piece2)
if port1 < 1 {
this.FailField("address", "端口号不能小于1")
}
if port1 > 65535 {
this.FailField("address", "端口号不能大于65535")
}
if port2 < 1 {
this.FailField("address", "端口号不能小于1")
}
if port2 > 65535 {
this.FailField("address", "端口号不能大于65535")
}
if port1 > port2 {
port1, port2 = port2, port1
}
return types.String(port1) + "-" + types.String(port2), port1, port2
}

View File

@@ -1,8 +1,11 @@
package apps
import (
"strconv"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
"github.com/iwind/TeaGo/actions"
)
@@ -11,15 +14,41 @@ type AppSettingsAction struct {
}
func (this *AppSettingsAction) Init() {
this.Nav("httpdns", "app", "")
this.Nav("httpdns", "app", "settings")
}
func (this *AppSettingsAction) RunGet(params struct {
AppId int64
Section string
}) {
httpdnsutils.AddLeftMenu(this.Parent())
app := pickApp(params.AppId)
// 顶部 tabbar
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "settings")
// 当前选中的 section
section := params.Section
if len(section) == 0 {
section = "basic"
}
this.Data["activeSection"] = section
appIdStr := strconv.FormatInt(params.AppId, 10)
this.Data["leftMenuItems"] = []map[string]interface{}{
{
"name": "基础配置",
"url": "/httpdns/apps/app/settings?appId=" + appIdStr + "&section=basic",
"isActive": section == "basic",
},
{
"name": "认证与密钥",
"url": "/httpdns/apps/app/settings?appId=" + appIdStr + "&section=auth",
"isActive": section == "auth",
},
}
settings := loadAppSettings(app)
this.Data["clusters"] = policies.LoadAvailableDeployClusters()
this.Data["app"] = app
this.Data["settings"] = settings
this.Show()
@@ -29,15 +58,23 @@ func (this *AppSettingsAction) RunPost(params struct {
AppId int64
AppStatus bool
PrimaryClusterId int64
BackupClusterId int64
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("appId", params.AppId).Gt(0, "please select app")
params.Must.Field("primaryClusterId", params.PrimaryClusterId).Gt(0, "please select primary cluster")
if params.BackupClusterId > 0 && params.BackupClusterId == params.PrimaryClusterId {
this.FailField("backupClusterId", "backup cluster must be different from primary cluster")
}
app := pickApp(params.AppId)
settings := loadAppSettings(app)
settings["appStatus"] = params.AppStatus
settings["primaryClusterId"] = params.PrimaryClusterId
settings["backupClusterId"] = params.BackupClusterId
// SNI strategy is fixed to level2 empty.
settings["sniPolicy"] = "level2"

View File

@@ -20,6 +20,8 @@ func defaultAppSettings(app maps.Map) maps.Map {
aesSecretPlain := randomPlainSecret("as")
return maps.Map{
"appId": app.GetString("appId"),
"primaryClusterId": app.GetInt64("clusterId"),
"backupClusterId": int64(0),
"signSecretPlain": signSecretPlain,
"signSecretMasked": maskSecret(signSecretPlain),
"signSecretUpdatedAt": "2026-02-20 12:30:00",
@@ -45,6 +47,8 @@ func defaultAppSettings(app maps.Map) maps.Map {
func cloneSettings(settings maps.Map) maps.Map {
return maps.Map{
"appId": settings.GetString("appId"),
"primaryClusterId": settings.GetInt64("primaryClusterId"),
"backupClusterId": settings.GetInt64("backupClusterId"),
"signSecretPlain": settings.GetString("signSecretPlain"),
"signSecretMasked": settings.GetString("signSecretMasked"),
"signSecretUpdatedAt": settings.GetString("signSecretUpdatedAt"),
@@ -90,6 +94,12 @@ func saveAppSettings(appId int64, settings maps.Map) {
appSettingsStore.Unlock()
}
func deleteAppSettings(appId int64) {
appSettingsStore.Lock()
delete(appSettingsStore.data, appId)
appSettingsStore.Unlock()
}
func resetSignSecret(app maps.Map) maps.Map {
settings := loadAppSettings(app)
signSecretPlain := randomPlainSecret("ss")
@@ -144,6 +154,19 @@ func maskSecret(secret string) string {
func ensureSettingsFields(settings maps.Map) bool {
changed := false
if settings.GetInt64("primaryClusterId") <= 0 {
settings["primaryClusterId"] = int64(1)
changed = true
}
if settings.GetInt64("backupClusterId") < 0 {
settings["backupClusterId"] = int64(0)
changed = true
}
if settings.GetInt64("backupClusterId") > 0 && settings.GetInt64("backupClusterId") == settings.GetInt64("primaryClusterId") {
settings["backupClusterId"] = int64(0)
changed = true
}
signSecretPlain := settings.GetString("signSecretPlain")
if len(signSecretPlain) == 0 {
signSecretPlain = randomPlainSecret("ss")

View File

@@ -19,17 +19,27 @@ func (this *CreatePopupAction) RunGet(params struct{}) {
clusters := policies.LoadAvailableDeployClusters()
this.Data["clusters"] = clusters
defaultClusterID := policies.LoadDefaultClusterID()
if defaultClusterID <= 0 && len(clusters) > 0 {
defaultClusterID = clusters[0].GetInt64("id")
defaultPrimaryClusterId := policies.LoadDefaultClusterID()
if defaultPrimaryClusterId <= 0 && len(clusters) > 0 {
defaultPrimaryClusterId = clusters[0].GetInt64("id")
}
this.Data["defaultClusterId"] = defaultClusterID
this.Data["defaultPrimaryClusterId"] = defaultPrimaryClusterId
defaultBackupClusterId := int64(0)
for _, cluster := range clusters {
clusterId := cluster.GetInt64("id")
if clusterId > 0 && clusterId != defaultPrimaryClusterId {
defaultBackupClusterId = clusterId
break
}
}
this.Data["defaultBackupClusterId"] = defaultBackupClusterId
// Mock users for dropdown
this.Data["users"] = []maps.Map{
{"id": int64(1), "name": "张三", "username": "zhangsan"},
{"id": int64(2), "name": "李四", "username": "lisi"},
{"id": int64(3), "name": "王五", "username": "wangwu"},
{"id": int64(1), "name": "User A", "username": "zhangsan"},
{"id": int64(2), "name": "User B", "username": "lisi"},
{"id": int64(3), "name": "User C", "username": "wangwu"},
}
this.Show()
@@ -37,13 +47,17 @@ func (this *CreatePopupAction) RunGet(params struct{}) {
func (this *CreatePopupAction) RunPost(params struct {
Name string
ClusterId int64
PrimaryClusterId int64
BackupClusterId int64
UserId int64
Must *actions.Must
CSRF *actionutils.CSRF
}) {
params.Must.Field("name", params.Name).Require("请输入应用名称")
params.Must.Field("clusterId", params.ClusterId).Gt(0, "请选择所属集群")
params.Must.Field("name", params.Name).Require("please input app name")
params.Must.Field("primaryClusterId", params.PrimaryClusterId).Gt(0, "please select primary cluster")
if params.BackupClusterId > 0 && params.BackupClusterId == params.PrimaryClusterId {
this.FailField("backupClusterId", "backup cluster must be different from primary cluster")
}
this.Success()
}

View File

@@ -21,6 +21,9 @@ func (this *CustomRecordsAction) RunGet(params struct {
httpdnsutils.AddLeftMenu(this.Parent())
app := pickApp(params.AppId)
// 自定义解析属于域名管理子页,顶部沿用应用 tabbar高亮域名列表
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "domains")
domains := mockDomains(app.GetInt64("id"))
domain := pickDomainFromDomains(domains, params.DomainId)
domainName := domain.GetString("name")
@@ -35,7 +38,6 @@ func (this *CustomRecordsAction) RunGet(params struct {
for _, record := range records {
record["lineText"] = buildLineText(record)
record["paramsText"] = buildParamsText(record)
record["recordValueText"] = buildRecordValueText(record)
}

View File

@@ -42,7 +42,6 @@ func (this *CustomRecordsCreatePopupAction) RunGet(params struct {
"weightEnabled": false,
"ttl": 30,
"isOn": true,
"sdnsParamsJson": "[]",
"recordItemsJson": `[{"type":"A","value":"","weight":100}]`,
}
@@ -65,9 +64,6 @@ func (this *CustomRecordsCreatePopupAction) RunGet(params struct {
record["ttl"] = existing.GetInt("ttl")
record["isOn"] = existing.GetBool("isOn")
sdnsParams, _ := existing["sdnsParams"].([]maps.Map)
record["sdnsParamsJson"] = marshalJSON(sdnsParams, "[]")
recordItems := make([]maps.Map, 0)
recordType := strings.ToUpper(strings.TrimSpace(existing.GetString("recordType")))
values, _ := existing["recordValues"].([]maps.Map)
@@ -140,7 +136,6 @@ func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
LineCountry string
RuleName string
SDNSParamsJSON string
RecordItemsJSON string
WeightEnabled bool
TTL int
@@ -154,7 +149,6 @@ func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
params.Domain = strings.TrimSpace(params.Domain)
params.LineScope = strings.ToLower(strings.TrimSpace(params.LineScope))
params.RuleName = strings.TrimSpace(params.RuleName)
params.SDNSParamsJSON = strings.TrimSpace(params.SDNSParamsJSON)
params.RecordItemsJSON = strings.TrimSpace(params.RecordItemsJSON)
domain := maps.Map{}
@@ -181,16 +175,6 @@ func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
return
}
sdnsParams, err := parseSDNSParamsJSON(params.SDNSParamsJSON)
if err != nil {
this.Fail(err.Error())
return
}
if len(sdnsParams) > 10 {
this.Fail("sdns params should be <= 10")
return
}
recordValues, err := parseRecordItemsJSON(params.RecordItemsJSON, params.WeightEnabled)
if err != nil {
this.Fail(err.Error())
@@ -250,7 +234,7 @@ func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
"lineContinent": lineContinent,
"lineCountry": lineCountry,
"ruleName": params.RuleName,
"sdnsParams": sdnsParams,
"sdnsParams": []maps.Map{},
"recordType": recordType,
"recordValues": recordValues,
"weightEnabled": params.WeightEnabled,
@@ -261,38 +245,6 @@ func (this *CustomRecordsCreatePopupAction) RunPost(params struct {
this.Success()
}
func parseSDNSParamsJSON(raw string) ([]maps.Map, error) {
if len(raw) == 0 {
return []maps.Map{}, nil
}
list := []maps.Map{}
if err := json.Unmarshal([]byte(raw), &list); err != nil {
return nil, fmt.Errorf("sdns params json is invalid")
}
result := make([]maps.Map, 0, len(list))
for _, item := range list {
name := strings.TrimSpace(item.GetString("name"))
value := strings.TrimSpace(item.GetString("value"))
if len(name) == 0 && len(value) == 0 {
continue
}
if len(name) < 2 || len(name) > 64 {
return nil, fmt.Errorf("sdns param name length should be in 2-64")
}
if len(value) < 1 || len(value) > 64 {
return nil, fmt.Errorf("sdns param value length should be in 1-64")
}
result = append(result, maps.Map{
"name": name,
"value": value,
})
}
return result, nil
}
func parseRecordItemsJSON(raw string, weightEnabled bool) ([]maps.Map, error) {
if len(raw) == 0 {
return []maps.Map{}, nil

View File

@@ -25,12 +25,7 @@ var customRecordStore = struct {
"lineRegion": "华东",
"lineProvince": "上海",
"ruleName": "上海电信灰度-v2",
"sdnsParams": []maps.Map{
{
"name": "app_ver",
"value": "2.3.1",
},
},
"sdnsParams": []maps.Map{},
"recordType": "A",
"recordValues": []maps.Map{{"type": "A", "value": "1.1.1.10", "weight": 100}},
"weightEnabled": false,
@@ -134,6 +129,12 @@ func deleteCustomRecord(appID int64, recordID int64) {
customRecordStore.data[appID] = filtered
}
func deleteCustomRecordsByApp(appID int64) {
customRecordStore.Lock()
defer customRecordStore.Unlock()
delete(customRecordStore.data, appID)
}
func toggleCustomRecord(appID int64, recordID int64, isOn bool) {
customRecordStore.Lock()
defer customRecordStore.Unlock()
@@ -207,27 +208,6 @@ func buildLineText(record maps.Map) string {
return strings.Join(finalParts, " / ")
}
func buildParamsText(record maps.Map) string {
params, ok := record["sdnsParams"].([]maps.Map)
if !ok || len(params) == 0 {
return "-"
}
parts := make([]string, 0, len(params))
for _, param := range params {
name := strings.TrimSpace(param.GetString("name"))
value := strings.TrimSpace(param.GetString("value"))
if len(name) == 0 || len(value) == 0 {
continue
}
parts = append(parts, name+"="+value)
}
if len(parts) == 0 {
return "-"
}
return strings.Join(parts, "; ")
}
func buildRecordValueText(record maps.Map) string {
values, ok := record["recordValues"].([]maps.Map)
if !ok || len(values) == 0 {

View File

@@ -0,0 +1,38 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
)
type DeleteAction struct {
actionutils.ParentAction
}
func (this *DeleteAction) Init() {
this.Nav("httpdns", "app", "delete")
}
func (this *DeleteAction) RunGet(params struct {
AppId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
app := pickApp(params.AppId)
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), app.GetInt64("id"), "delete")
this.Data["app"] = app
this.Data["domainCount"] = len(mockDomains(app.GetInt64("id")))
this.Show()
}
func (this *DeleteAction) RunPost(params struct {
AppId int64
}) {
if params.AppId > 0 {
if deleteApp(params.AppId) {
deleteAppSettings(params.AppId)
deleteCustomRecordsByApp(params.AppId)
}
}
this.Success()
}

View File

@@ -10,7 +10,7 @@ type DomainsAction struct {
}
func (this *DomainsAction) Init() {
this.Nav("httpdns", "app", "")
this.Nav("httpdns", "app", "domains")
}
func (this *DomainsAction) RunGet(params struct {
@@ -19,6 +19,9 @@ func (this *DomainsAction) RunGet(params struct {
httpdnsutils.AddLeftMenu(this.Parent())
app := pickApp(params.AppId)
// 构建顶部 tabbar
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "domains")
domains := mockDomains(app.GetInt64("id"))
for _, domain := range domains {
domainName := domain.GetString("name")

View File

@@ -15,6 +15,7 @@ func init() {
Prefix("/httpdns/apps").
Get("", new(IndexAction)).
Get("/app", new(AppAction)).
Get("/sdk", new(SDKAction)).
GetPost("/app/settings", new(AppSettingsAction)).
Post("/app/settings/toggleSignEnabled", new(AppSettingsToggleSignEnabledAction)).
Post("/app/settings/resetSignSecret", new(AppSettingsResetSignSecretAction)).
@@ -22,6 +23,7 @@ func init() {
Get("/domains", new(DomainsAction)).
Get("/customRecords", new(CustomRecordsAction)).
GetPost("/createPopup", new(CreatePopupAction)).
GetPost("/delete", new(DeleteAction)).
GetPost("/domains/createPopup", new(DomainsCreatePopupAction)).
Post("/domains/delete", new(DomainsDeleteAction)).
GetPost("/customRecords/createPopup", new(CustomRecordsCreatePopupAction)).

View File

@@ -2,15 +2,23 @@ package apps
import (
"strings"
"sync"
"github.com/iwind/TeaGo/maps"
)
func mockApps() []maps.Map {
var appStore = struct {
sync.RWMutex
data []maps.Map
}{
data: defaultMockApps(),
}
func defaultMockApps() []maps.Map {
return []maps.Map{
{
"id": int64(1),
"name": "主站移动业务",
"name": "\u4e3b\u7ad9\u79fb\u52a8\u4e1a\u52a1",
"appId": "ab12xc34s2",
"clusterId": int64(1),
"domainCount": 3,
@@ -20,12 +28,12 @@ func mockApps() []maps.Map {
"pinningMode": "report",
"sanMode": "strict",
"riskLevel": "medium",
"riskSummary": "Pinning 处于观察模式",
"riskSummary": "Pinning \u5904\u4e8e\u89c2\u5bdf\u6a21\u5f0f",
"secretVersion": "v2026.02.20",
},
{
"id": int64(2),
"name": "视频网关业务",
"name": "\u89c6\u9891\u7f51\u5173\u4e1a\u52a1",
"appId": "vd8992ksm1",
"clusterId": int64(2),
"domainCount": 1,
@@ -35,12 +43,12 @@ func mockApps() []maps.Map {
"pinningMode": "enforce",
"sanMode": "strict",
"riskLevel": "low",
"riskSummary": "已启用强校验",
"riskSummary": "\u5df2\u542f\u7528\u5f3a\u6821\u9a8c",
"secretVersion": "v2026.02.18",
},
{
"id": int64(3),
"name": "海外灰度测试",
"name": "\u6d77\u5916\u7070\u5ea6\u6d4b\u8bd5",
"appId": "ov7711hkq9",
"clusterId": int64(1),
"domainCount": 2,
@@ -50,12 +58,57 @@ func mockApps() []maps.Map {
"pinningMode": "off",
"sanMode": "report",
"riskLevel": "high",
"riskSummary": "应用关闭且证书策略偏弱",
"riskSummary": "\u5e94\u7528\u5173\u95ed\u4e14\u8bc1\u4e66\u7b56\u7565\u504f\u5f31",
"secretVersion": "v2026.01.30",
},
}
}
func cloneMap(src maps.Map) maps.Map {
dst := maps.Map{}
for k, v := range src {
dst[k] = v
}
return dst
}
func cloneApps(apps []maps.Map) []maps.Map {
result := make([]maps.Map, 0, len(apps))
for _, app := range apps {
result = append(result, cloneMap(app))
}
return result
}
func mockApps() []maps.Map {
appStore.RLock()
defer appStore.RUnlock()
return cloneApps(appStore.data)
}
func deleteApp(appID int64) bool {
if appID <= 0 {
return false
}
appStore.Lock()
defer appStore.Unlock()
found := false
filtered := make([]maps.Map, 0, len(appStore.data))
for _, app := range appStore.data {
if app.GetInt64("id") == appID {
found = true
continue
}
filtered = append(filtered, app)
}
if found {
appStore.data = filtered
}
return found
}
func filterApps(keyword string, riskLevel string, ecsMode string, pinningMode string) []maps.Map {
all := mockApps()
if len(keyword) == 0 && len(riskLevel) == 0 && len(ecsMode) == 0 && len(pinningMode) == 0 {
@@ -89,6 +142,15 @@ func filterApps(keyword string, riskLevel string, ecsMode string, pinningMode st
func pickApp(appID int64) maps.Map {
apps := mockApps()
if len(apps) == 0 {
return maps.Map{
"id": int64(0),
"name": "",
"appId": "",
"clusterId": int64(0),
}
}
if appID <= 0 {
return apps[0]
}

View File

@@ -0,0 +1,27 @@
package apps
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
)
type SDKAction struct {
actionutils.ParentAction
}
func (this *SDKAction) Init() {
this.Nav("httpdns", "app", "sdk")
}
func (this *SDKAction) RunGet(params struct {
AppId int64
}) {
httpdnsutils.AddLeftMenu(this.Parent())
app := pickApp(params.AppId)
// 构建顶部 tabbar
httpdnsutils.AddAppTabbar(this.Parent(), app.GetString("name"), params.AppId, "sdk")
this.Data["app"] = app
this.Show()
}

View File

@@ -21,6 +21,10 @@ func (this *ClusterAction) RunGet(params struct {
}) {
httpdnsutils.AddLeftMenu(this.Parent())
cluster := pickCluster(params.ClusterId)
// 构建顶部 tabbar
httpdnsutils.AddClusterTabbar(this.Parent(), cluster.GetString("name"), params.ClusterId, "node")
this.Data["clusterId"] = params.ClusterId
this.Data["cluster"] = cluster
this.Data["installState"] = params.InstalledState

View File

@@ -1,8 +1,17 @@
package clusters
import (
"encoding/json"
"strconv"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
)
type ClusterSettingsAction struct {
@@ -15,35 +24,184 @@ func (this *ClusterSettingsAction) Init() {
func (this *ClusterSettingsAction) RunGet(params struct {
ClusterId int64
Section string
}) {
httpdnsutils.AddLeftMenu(this.Parent())
cluster := pickCluster(params.ClusterId)
installDir := cluster.GetString("installDir")
if len(installDir) == 0 {
installDir = "/opt/edge-httpdns"
settings := loadClusterSettings(cluster)
cluster["name"] = settings.GetString("name")
// 构建顶部 tabbar
httpdnsutils.AddClusterTabbar(this.Parent(), cluster.GetString("name"), params.ClusterId, "setting")
// 当前选中的 section
section := params.Section
if len(section) == 0 {
section = "basic"
}
this.Data["activeSection"] = section
// 左侧菜单
cid := strconv.FormatInt(params.ClusterId, 10)
this.Data["leftMenuItems"] = []map[string]interface{}{
{"name": "基础设置", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "&section=basic", "isActive": section == "basic"},
{"name": "TLS", "url": "/httpdns/clusters/cluster/settings?clusterId=" + cid + "&section=tls", "isActive": section == "tls"},
}
settings["isDefaultCluster"] = (policies.LoadDefaultClusterID() == cluster.GetInt64("id"))
this.Data["cluster"] = cluster
this.Data["settings"] = map[string]interface{}{
"region": cluster.GetString("region"),
"gatewayDomain": cluster.GetString("gatewayDomain"),
"cacheTtl": cluster.GetInt("cacheTtl"),
"fallbackTimeout": cluster.GetInt("fallbackTimeout"),
"installDir": installDir,
"isOn": cluster.GetBool("isOn"),
// 构造前端需要的 tlsConfig 格式
var listenAddresses []*serverconfigs.NetworkAddressConfig
listenAddrsRaw := settings.GetString("listenAddrsJSON")
if len(listenAddrsRaw) > 0 {
_ = json.Unmarshal([]byte(listenAddrsRaw), &listenAddresses)
} else {
// 默认 443 端口
listenAddresses = []*serverconfigs.NetworkAddressConfig{
{
Protocol: serverconfigs.ProtocolHTTPS,
Host: "",
PortRange: "443",
},
}
}
// 构造前端需要的 SSLPolicy
var sslPolicy *sslconfigs.SSLPolicy
originCertPem := settings.GetString("originCertPem")
originKeyPem := settings.GetString("originKeyPem")
if len(originCertPem) > 0 && len(originKeyPem) > 0 {
sslPolicy = &sslconfigs.SSLPolicy{
IsOn: true,
MinVersion: settings.GetString("tlsMinVersion"),
CipherSuitesIsOn: settings.GetBool("tlsCipherSuitesOn"),
OCSPIsOn: settings.GetBool("tlsOcspOn"),
ClientAuthType: int(settings.GetInt32("tlsClientAuthType")),
Certs: []*sslconfigs.SSLCertConfig{
{
IsOn: true,
CertData: []byte(originCertPem),
KeyData: []byte(originKeyPem),
},
},
}
} else {
sslPolicy = &sslconfigs.SSLPolicy{
IsOn: true,
}
}
this.Data["tlsConfig"] = maps.Map{
"isOn": true,
"listen": listenAddresses,
"sslPolicy": sslPolicy,
}
this.Data["cluster"] = cluster
this.Data["settings"] = settings
this.Show()
}
func (this *ClusterSettingsAction) RunPost(params struct {
ClusterId int64
Name string
Region string
GatewayDomain string
CacheTtl int32
FallbackTimeout int32
InstallDir string
IsOn bool
IsDefaultCluster bool
Addresses []byte
SslPolicyJSON []byte
Must *actions.Must
CSRF *actionutils.CSRF
}) {
// Mock successful save
params.Must.Field("clusterId", params.ClusterId).Gt(0, "please select cluster")
params.Must.Field("name", params.Name).Require("please input cluster name")
params.Must.Field("gatewayDomain", params.GatewayDomain).Require("please input service domain")
params.Must.Field("cacheTtl", params.CacheTtl).Gt(0, "cache ttl should be greater than 0")
params.Must.Field("fallbackTimeout", params.FallbackTimeout).Gt(0, "fallback timeout should be greater than 0")
params.Must.Field("installDir", params.InstallDir).Require("please input install dir")
if params.IsDefaultCluster && !params.IsOn {
this.Fail("默认集群必须保持启用状态")
return
}
cluster := pickCluster(params.ClusterId)
settings := loadClusterSettings(cluster)
settings["name"] = strings.TrimSpace(params.Name)
settings["gatewayDomain"] = strings.TrimSpace(params.GatewayDomain)
settings["cacheTtl"] = int(params.CacheTtl)
settings["fallbackTimeout"] = int(params.FallbackTimeout)
settings["installDir"] = strings.TrimSpace(params.InstallDir)
settings["isOn"] = params.IsOn
// 处理地址
var addresses = []*serverconfigs.NetworkAddressConfig{}
if len(params.Addresses) > 0 {
err := json.Unmarshal(params.Addresses, &addresses)
if err != nil {
this.Fail("端口地址解析失败:" + err.Error())
}
addressesJSON, _ := json.Marshal(addresses)
settings["listenAddrsJSON"] = string(addressesJSON)
}
// 处理 SSL 配置
var originCertPem = ""
var originKeyPem = ""
var tlsMinVersion = "TLS 1.1"
var tlsCipherSuitesOn = false
var tlsOcspOn = false
var tlsClientAuthType = sslconfigs.SSLClientAuthType(0)
if len(params.SslPolicyJSON) > 0 {
sslPolicy := &sslconfigs.SSLPolicy{}
err := json.Unmarshal(params.SslPolicyJSON, sslPolicy)
if err == nil {
tlsMinVersion = sslPolicy.MinVersion
tlsCipherSuitesOn = sslPolicy.CipherSuitesIsOn
tlsOcspOn = sslPolicy.OCSPIsOn
tlsClientAuthType = sslconfigs.SSLClientAuthType(sslPolicy.ClientAuthType)
if len(sslPolicy.Certs) > 0 {
cert := sslPolicy.Certs[0]
originCertPem = string(cert.CertData)
originKeyPem = string(cert.KeyData)
}
}
}
if len(originCertPem) == 0 || len(originKeyPem) == 0 {
this.Fail("请上传或选择证书")
}
settings["originHttps"] = true
settings["originCertPem"] = originCertPem
settings["originKeyPem"] = originKeyPem
if len(tlsMinVersion) == 0 {
tlsMinVersion = "TLS 1.1"
}
settings["tlsMinVersion"] = tlsMinVersion
settings["tlsCipherSuitesOn"] = tlsCipherSuitesOn
settings["tlsOcspOn"] = tlsOcspOn
settings["tlsClientAuthType"] = int(tlsClientAuthType)
settings["lastModifiedAt"] = nowDateTime()
settings["certUpdatedAt"] = nowDateTime()
saveClusterSettings(params.ClusterId, settings)
currentDefaultClusterId := policies.LoadDefaultClusterID()
if params.IsDefaultCluster {
policies.SaveDefaultClusterID(params.ClusterId)
} else if currentDefaultClusterId == params.ClusterId {
policies.SaveDefaultClusterID(0)
}
this.Success()
}

View File

@@ -0,0 +1,110 @@
package clusters
import (
"strings"
"sync"
"time"
"github.com/iwind/TeaGo/maps"
)
var clusterSettingsStore = struct {
sync.RWMutex
data map[int64]maps.Map
}{
data: map[int64]maps.Map{},
}
func defaultClusterSettings(cluster maps.Map) maps.Map {
installDir := strings.TrimSpace(cluster.GetString("installDir"))
if len(installDir) == 0 {
installDir = "/opt/edge-httpdns"
}
return maps.Map{
"name": cluster.GetString("name"),
"gatewayDomain": strings.TrimSpace(cluster.GetString("gatewayDomain")),
"cacheTtl": cluster.GetInt("cacheTtl"),
"fallbackTimeout": cluster.GetInt("fallbackTimeout"),
"installDir": installDir,
"isOn": cluster.GetBool("isOn"),
"originHttps": true,
"originCertPem": "",
"originKeyPem": "",
"tlsMinVersion": "TLS 1.1",
"tlsCipherSuitesOn": false,
"tlsOcspOn": false,
"tlsClientAuthType": 0,
"certUpdatedAt": "",
"lastModifiedAt": "",
}
}
func cloneClusterSettings(settings maps.Map) maps.Map {
return maps.Map{
"name": settings.GetString("name"),
"gatewayDomain": settings.GetString("gatewayDomain"),
"cacheTtl": settings.GetInt("cacheTtl"),
"fallbackTimeout": settings.GetInt("fallbackTimeout"),
"installDir": settings.GetString("installDir"),
"isOn": settings.GetBool("isOn"),
"originHttps": true,
"originCertPem": settings.GetString("originCertPem"),
"originKeyPem": settings.GetString("originKeyPem"),
"tlsMinVersion": settings.GetString("tlsMinVersion"),
"tlsCipherSuitesOn": settings.GetBool("tlsCipherSuitesOn"),
"tlsOcspOn": settings.GetBool("tlsOcspOn"),
"tlsClientAuthType": settings.GetInt("tlsClientAuthType"),
"certUpdatedAt": settings.GetString("certUpdatedAt"),
"lastModifiedAt": settings.GetString("lastModifiedAt"),
}
}
func loadClusterSettings(cluster maps.Map) maps.Map {
clusterId := cluster.GetInt64("id")
clusterSettingsStore.RLock()
settings, ok := clusterSettingsStore.data[clusterId]
clusterSettingsStore.RUnlock()
if ok {
if len(settings.GetString("tlsMinVersion")) == 0 {
settings["tlsMinVersion"] = "TLS 1.1"
}
return cloneClusterSettings(settings)
}
settings = defaultClusterSettings(cluster)
saveClusterSettings(clusterId, settings)
return cloneClusterSettings(settings)
}
func saveClusterSettings(clusterId int64, settings maps.Map) {
clusterSettingsStore.Lock()
clusterSettingsStore.data[clusterId] = cloneClusterSettings(settings)
clusterSettingsStore.Unlock()
}
func applyClusterSettingsOverrides(cluster maps.Map) {
clusterId := cluster.GetInt64("id")
if clusterId <= 0 {
return
}
clusterSettingsStore.RLock()
settings, ok := clusterSettingsStore.data[clusterId]
clusterSettingsStore.RUnlock()
if !ok {
return
}
cluster["name"] = settings.GetString("name")
cluster["gatewayDomain"] = settings.GetString("gatewayDomain")
cluster["cacheTtl"] = settings.GetInt("cacheTtl")
cluster["fallbackTimeout"] = settings.GetInt("fallbackTimeout")
cluster["installDir"] = settings.GetString("installDir")
cluster["isOn"] = settings.GetBool("isOn")
}
func nowDateTime() string {
return time.Now().Format("2006-01-02 15:04:05")
}

View File

@@ -3,7 +3,6 @@ package clusters
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
"github.com/iwind/TeaGo/maps"
)
type CreateNodeAction struct {
@@ -11,14 +10,18 @@ type CreateNodeAction struct {
}
func (this *CreateNodeAction) Init() {
this.Nav("", "node", "createNode")
this.SecondMenu("nodes")
this.Nav("httpdns", "cluster", "createNode")
}
func (this *CreateNodeAction) RunGet(params struct{ ClusterId int64 }) {
httpdnsutils.AddLeftMenu(this.Parent())
cluster := pickCluster(params.ClusterId)
// 构建顶部 tabbar
httpdnsutils.AddClusterTabbar(this.Parent(), cluster.GetString("name"), params.ClusterId, "node")
this.Data["clusterId"] = params.ClusterId
this.Data["cluster"] = maps.Map{"id": params.ClusterId, "name": "Mock Cluster"}
this.Data["cluster"] = cluster
this.Show()
}

View File

@@ -18,6 +18,10 @@ func (this *DeleteAction) RunGet(params struct {
}) {
httpdnsutils.AddLeftMenu(this.Parent())
cluster := pickCluster(params.ClusterId)
// 构建顶部 tabbar
httpdnsutils.AddClusterTabbar(this.Parent(), cluster.GetString("name"), params.ClusterId, "delete")
this.Data["cluster"] = cluster
this.Show()
}

View File

@@ -3,7 +3,7 @@ package clusters
import "github.com/iwind/TeaGo/maps"
func mockClusters() []maps.Map {
return []maps.Map{
clusters := []maps.Map{
{
"id": int64(1),
"name": "gateway-cn-hz",
@@ -31,6 +31,12 @@ func mockClusters() []maps.Map {
"isOn": true,
},
}
for _, cluster := range clusters {
applyClusterSettingsOverrides(cluster)
}
return clusters
}
func pickCluster(clusterId int64) maps.Map {

View File

@@ -1,66 +1,15 @@
package guide
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
httpdnsApps "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/apps"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
httpdnsPolicies "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/policies"
"github.com/iwind/TeaGo/maps"
)
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("httpdns", "guide", "")
this.Nav("httpdns", "app", "")
}
func (this *IndexAction) RunGet(params struct{}) {
httpdnsutils.AddLeftMenu(this.Parent())
apps := []maps.Map{
{
"id": int64(1),
"name": "主站移动业务",
"appId": "ab12xc34s2",
"clusterId": int64(1),
},
{
"id": int64(2),
"name": "视频网关业务",
"appId": "vd8992ksm1",
"clusterId": int64(2),
},
{
"id": int64(3),
"name": "海外灰度测试",
"appId": "ov7711hkq9",
"clusterId": int64(1),
},
}
for _, app := range apps {
clusterID := app.GetInt64("clusterId")
app["gatewayDomain"] = httpdnsPolicies.LoadClusterGatewayByID(clusterID)
settings := httpdnsApps.LoadAppSettingsByAppID(app.GetString("appId"))
if settings == nil {
app["signSecret"] = ""
app["signSecretMasked"] = ""
app["aesSecret"] = ""
app["aesSecretMasked"] = ""
app["signEnabled"] = false
continue
}
app["signSecret"] = settings.GetString("signSecretPlain")
app["signSecretMasked"] = settings.GetString("signSecretMasked")
app["aesSecret"] = settings.GetString("aesSecretPlain")
app["aesSecretMasked"] = settings.GetString("aesSecretMasked")
app["signEnabled"] = settings.GetBool("signEnabled")
}
this.Data["apps"] = apps
this.Show()
this.RedirectURL("/httpdns/apps")
}

View File

@@ -1,6 +1,8 @@
package httpdnsutils
import (
"strconv"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
@@ -11,39 +13,55 @@ func AddLeftMenu(action *actionutils.ParentAction) {
action.Data["teaSubMenu"] = tab
action.Data["leftMenuItems"] = []maps.Map{
{
"name": "集群管理",
"name": "\u96c6\u7fa4\u7ba1\u7406",
"url": "/httpdns/clusters",
"isActive": tab == "cluster",
},
{
"name": "全局配置",
"url": "/httpdns/policies",
"isActive": tab == "policy",
},
{
"name": "应用管理",
"name": "\u5e94\u7528\u7ba1\u7406",
"url": "/httpdns/apps",
"isActive": tab == "app",
},
{
"name": "SDK接入引导",
"url": "/httpdns/guide",
"isActive": tab == "guide",
},
{
"name": "解析日志",
"name": "\u8bbf\u95ee\u65e5\u5fd7",
"url": "/httpdns/resolveLogs",
"isActive": tab == "resolveLogs",
},
{
"name": "运行日志",
"name": "\u8fd0\u884c\u65e5\u5fd7",
"url": "/httpdns/runtimeLogs",
"isActive": tab == "runtimeLogs",
},
{
"name": "解析测试",
"name": "\u89e3\u6790\u6d4b\u8bd5",
"url": "/httpdns/sandbox",
"isActive": tab == "sandbox",
},
}
}
// AddClusterTabbar builds the top tabbar on cluster pages.
func AddClusterTabbar(action *actionutils.ParentAction, clusterName string, clusterId int64, selectedTab string) {
cid := strconv.FormatInt(clusterId, 10)
tabbar := actionutils.NewTabbar()
tabbar.Add("", "", "/httpdns/clusters", "arrow left", false)
titleItem := tabbar.Add(clusterName, "", "/httpdns/clusters/cluster?clusterId="+cid, "angle right", true)
titleItem.IsTitle = true
tabbar.Add("\u8282\u70b9\u5217\u8868", "", "/httpdns/clusters/cluster?clusterId="+cid, "server", selectedTab == "node")
tabbar.Add("\u96c6\u7fa4\u8bbe\u7f6e", "", "/httpdns/clusters/cluster/settings?clusterId="+cid, "setting", selectedTab == "setting")
tabbar.Add("\u5220\u9664\u96c6\u7fa4", "", "/httpdns/clusters/delete?clusterId="+cid, "trash", selectedTab == "delete")
actionutils.SetTabbar(action, tabbar)
}
// AddAppTabbar builds the top tabbar on app pages.
func AddAppTabbar(action *actionutils.ParentAction, appName string, appId int64, selectedTab string) {
aid := strconv.FormatInt(appId, 10)
tabbar := actionutils.NewTabbar()
tabbar.Add("", "", "/httpdns/apps", "arrow left", false)
titleItem := tabbar.Add(appName, "", "/httpdns/apps/domains?appId="+aid, "angle right", true)
titleItem.IsTitle = true
tabbar.Add("\u57df\u540d\u5217\u8868", "", "/httpdns/apps/domains?appId="+aid, "list", selectedTab == "domains")
tabbar.Add("\u5e94\u7528\u8bbe\u7f6e", "", "/httpdns/apps/app/settings?appId="+aid, "setting", selectedTab == "settings")
tabbar.Add("\u5220\u9664\u5e94\u7528", "", "/httpdns/apps/delete?appId="+aid, "trash", selectedTab == "delete")
actionutils.SetTabbar(action, tabbar)
}

View File

@@ -14,6 +14,7 @@ func init() {
Data("teaSubMenu", "cluster").
Prefix("/httpdns").
Get("", new(IndexAction)).
GetPost("/addPortPopup", new(AddPortPopupAction)).
EndAll()
})
}

View File

@@ -24,7 +24,6 @@ func (this *IndexAction) RunGet(params struct{}) {
func (this *IndexAction) RunPost(params struct {
DefaultClusterId int64
EnableUserDomainVerify bool
DefaultTTL int
DefaultFallbackMs int
@@ -50,7 +49,7 @@ func (this *IndexAction) RunPost(params struct {
saveGlobalPolicies(maps.Map{
"defaultClusterId": params.DefaultClusterId,
"enableUserDomainVerify": params.EnableUserDomainVerify,
"enableUserDomainVerify": true,
"defaultTTL": params.DefaultTTL,
"defaultFallbackMs": params.DefaultFallbackMs,
})

View File

@@ -51,7 +51,7 @@ func loadGlobalPolicies() maps.Map {
return maps.Map{
"defaultClusterId": globalPoliciesStore.data.GetInt64("defaultClusterId"),
"enableUserDomainVerify": globalPoliciesStore.data.GetBool("enableUserDomainVerify"),
"enableUserDomainVerify": true,
"defaultTTL": globalPoliciesStore.data.GetInt("defaultTTL"),
"defaultFallbackMs": globalPoliciesStore.data.GetInt("defaultFallbackMs"),
}
@@ -61,7 +61,7 @@ func saveGlobalPolicies(policies maps.Map) {
globalPoliciesStore.Lock()
globalPoliciesStore.data = maps.Map{
"defaultClusterId": policies.GetInt64("defaultClusterId"),
"enableUserDomainVerify": policies.GetBool("enableUserDomainVerify"),
"enableUserDomainVerify": true,
"defaultTTL": policies.GetInt("defaultTTL"),
"defaultFallbackMs": policies.GetInt("defaultFallbackMs"),
}
@@ -110,6 +110,14 @@ func LoadDefaultClusterID() int64 {
return globalPoliciesStore.data.GetInt64("defaultClusterId")
}
// SaveDefaultClusterID updates default deploy cluster id.
// Pass 0 to clear and let caller fallback to first available cluster.
func SaveDefaultClusterID(clusterID int64) {
globalPoliciesStore.Lock()
globalPoliciesStore.data["defaultClusterId"] = clusterID
globalPoliciesStore.Unlock()
}
// LoadClusterGatewayByID returns gateway domain for the selected cluster.
func LoadClusterGatewayByID(clusterID int64) string {
clusters := loadAvailableDeployClusters()

View File

@@ -25,8 +25,16 @@ func (this *IndexAction) RunGet(params struct {
}) {
httpdnsutils.AddLeftMenu(this.Parent())
if params.ClusterId > 0 {
this.Data["clusterId"] = params.ClusterId
} else {
this.Data["clusterId"] = ""
}
if params.NodeId > 0 {
this.Data["nodeId"] = params.NodeId
} else {
this.Data["nodeId"] = ""
}
this.Data["appId"] = params.AppId
this.Data["domain"] = params.Domain
this.Data["status"] = params.Status

View File

@@ -25,8 +25,16 @@ func (this *IndexAction) RunGet(params struct {
}) {
httpdnsutils.AddLeftMenu(this.Parent())
if params.ClusterId > 0 {
this.Data["clusterId"] = params.ClusterId
} else {
this.Data["clusterId"] = ""
}
if params.NodeId > 0 {
this.Data["nodeId"] = params.NodeId
} else {
this.Data["nodeId"] = ""
}
this.Data["dayFrom"] = params.DayFrom
this.Data["dayTo"] = params.DayTo
this.Data["level"] = params.Level
@@ -123,7 +131,6 @@ func (this *IndexAction) RunGet(params struct {
}
if len(keyword) > 0 {
if !strings.Contains(strings.ToLower(log["tag"].(string)), keyword) &&
!strings.Contains(strings.ToLower(log["module"].(string)), keyword) &&
!strings.Contains(strings.ToLower(log["description"].(string)), keyword) &&
!strings.Contains(strings.ToLower(log["nodeName"].(string)), keyword) {
continue

View File

@@ -1,12 +1,9 @@
package sandbox
import (
"encoding/json"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
@@ -21,18 +18,10 @@ func (this *TestAction) RunPost(params struct {
Domain string
ClientIp string
Qtype string
SDNSParamsJSON string
}) {
if len(params.ClientIp) == 0 {
params.ClientIp = "203.0.113.100"
}
params.SDNSParamsJSON = strings.TrimSpace(params.SDNSParamsJSON)
sdnsParams, err := parseSDNSParamsJSON(params.SDNSParamsJSON)
if err != nil {
this.Fail(err.Error())
return
}
clientSubnet := this.maskSubnet(params.ClientIp, 24, 56)
if len(clientSubnet) == 0 {
@@ -51,9 +40,6 @@ func (this *TestAction) RunPost(params struct {
query.Set("dn", params.Domain)
query.Set("cip", params.ClientIp)
query.Set("qtype", params.Qtype)
for _, item := range sdnsParams {
query.Add("sdns_"+item.GetString("name"), item.GetString("value"))
}
requestURL := "https://api.httpdns.example.com/resolve?" + query.Encode()
this.Data["result"] = maps.Map{
@@ -65,7 +51,6 @@ func (this *TestAction) RunPost(params struct {
"client_ip": params.ClientIp,
"client_region": "中国, 上海, 上海",
"line_name": "默认线路",
"sdns_params": sdnsParams,
"ips": []string{"203.0.113.10", "203.0.113.11"},
"records": []maps.Map{
{
@@ -108,44 +93,6 @@ func (this *TestAction) RunPost(params struct {
this.Success()
}
func parseSDNSParamsJSON(raw string) ([]maps.Map, error) {
if len(raw) == 0 {
return []maps.Map{}, nil
}
list := []maps.Map{}
if err := json.Unmarshal([]byte(raw), &list); err != nil {
return nil, err
}
if len(list) > 10 {
return nil, fmt.Errorf("sdns params should be <= 10")
}
result := make([]maps.Map, 0, len(list))
for _, item := range list {
name := strings.TrimSpace(item.GetString("name"))
value := strings.TrimSpace(item.GetString("value"))
if len(name) == 0 && len(value) == 0 {
continue
}
if len(name) == 0 || len(name) > 64 {
return nil, fmt.Errorf("sdns param name should be in 1-64")
}
if len(value) == 0 || len(value) > 64 {
return nil, fmt.Errorf("sdns param value should be in 1-64")
}
result = append(result, maps.Map{
"name": name,
"value": value,
})
}
if len(result) > 10 {
return nil, fmt.Errorf("sdns params should be <= 10")
}
return result, nil
}
func (this *TestAction) maskSubnet(ip string, ipv4Prefix int, ipv6Prefix int) string {
parsed := net.ParseIP(ip)
if parsed == nil {

View File

@@ -113,50 +113,6 @@ func FindAllMenuMaps(langCode string, nodeLogsType string, countUnreadNodeLogs i
},
},
},
{
"code": "httpdns",
"module": configloaders.AdminModuleCodeHttpDNS,
"name": "HTTPDNS",
"subtitle": "",
"icon": "shield alternate",
"subItems": []maps.Map{
{
"name": "集群管理",
"url": "/httpdns/clusters",
"code": "cluster",
},
{
"name": "全局配置",
"url": "/httpdns/policies",
"code": "policy",
},
{
"name": "应用管理",
"url": "/httpdns/apps",
"code": "app",
},
{
"name": "SDK接入引导",
"url": "/httpdns/guide",
"code": "guide",
},
{
"name": "解析日志",
"url": "/httpdns/resolveLogs",
"code": "resolveLogs",
},
{
"name": "运行日志",
"url": "/httpdns/runtimeLogs",
"code": "runtimeLogs",
},
{
"name": "解析测试",
"url": "/httpdns/sandbox",
"code": "sandbox",
},
},
},
{
"code": "dns",
"module": configloaders.AdminModuleCodeDNS,
@@ -181,6 +137,40 @@ func FindAllMenuMaps(langCode string, nodeLogsType string, countUnreadNodeLogs i
},
},
},
{
"code": "httpdns",
"module": configloaders.AdminModuleCodeHttpDNS,
"name": "HTTPDNS",
"subtitle": "",
"icon": "shield alternate",
"subItems": []maps.Map{
{
"name": "集群管理",
"url": "/httpdns/clusters",
"code": "cluster",
},
{
"name": "应用管理",
"url": "/httpdns/apps",
"code": "app",
},
{
"name": "访问日志",
"url": "/httpdns/resolveLogs",
"code": "resolveLogs",
},
{
"name": "运行日志",
"url": "/httpdns/runtimeLogs",
"code": "runtimeLogs",
},
{
"name": "解析测试",
"url": "/httpdns/sandbox",
"code": "sandbox",
},
},
},
{
"code": "users",
"module": configloaders.AdminModuleCodeUser,

View File

@@ -178,50 +178,6 @@ func FindAllMenuMaps(langCode string, nodeLogsType string, countUnreadNodeLogs i
},
},
},
{
"code": "httpdns",
"module": configloaders.AdminModuleCodeHttpDNS,
"name": "HTTPDNS",
"subtitle": "",
"icon": "shield alternate",
"subItems": []maps.Map{
{
"name": "集群管理",
"url": "/httpdns/clusters",
"code": "cluster",
},
{
"name": "全局配置",
"url": "/httpdns/policies",
"code": "policy",
},
{
"name": "应用管理",
"url": "/httpdns/apps",
"code": "app",
},
{
"name": "SDK接入引导",
"url": "/httpdns/guide",
"code": "guide",
},
{
"name": "解析日志",
"url": "/httpdns/resolveLogs",
"code": "resolveLogs",
},
{
"name": "运行日志",
"url": "/httpdns/runtimeLogs",
"code": "runtimeLogs",
},
{
"name": "解析测试",
"url": "/httpdns/sandbox",
"code": "sandbox",
},
},
},
{
"code": "dns",
"module": configloaders.AdminModuleCodeDNS,
@@ -319,6 +275,40 @@ func FindAllMenuMaps(langCode string, nodeLogsType string, countUnreadNodeLogs i
},
},
},
{
"code": "httpdns",
"module": configloaders.AdminModuleCodeHttpDNS,
"name": "HTTPDNS",
"subtitle": "",
"icon": "shield alternate",
"subItems": []maps.Map{
{
"name": "集群管理",
"url": "/httpdns/clusters",
"code": "cluster",
},
{
"name": "应用管理",
"url": "/httpdns/apps",
"code": "app",
},
{
"name": "访问日志",
"url": "/httpdns/resolveLogs",
"code": "resolveLogs",
},
{
"name": "运行日志",
"url": "/httpdns/runtimeLogs",
"code": "runtimeLogs",
},
{
"name": "解析测试",
"url": "/httpdns/sandbox",
"code": "sandbox",
},
},
},
{
"code": "users",
"module": configloaders.AdminModuleCodeUser,
@@ -477,3 +467,4 @@ func FindAllMenuMaps(langCode string, nodeLogsType string, countUnreadNodeLogs i
},
}
}

View File

@@ -1,60 +1,70 @@
{$layout}
{$template "menu"}
{$template "/left_menu_with_menu"}
<style>
.httpdns-settings-grid {
margin-top: 0 !important;
}
.httpdns-settings-grid .left-nav-column {
max-width: 220px;
}
.httpdns-settings-grid .right-form-column {
padding-left: .3em !important;
}
.httpdns-side-menu .item {
padding-top: .8em !important;
padding-bottom: .8em !important;
}
.httpdns-mini-action {
.httpdns-mini-action {
display: inline-block;
font-size: 12px;
color: #6b7280;
margin-left: .55em;
line-height: 1.6;
}
.httpdns-mini-action:hover {
}
.httpdns-mini-action:hover {
color: #1e70bf;
}
.httpdns-mini-action .icon {
}
.httpdns-mini-action .icon {
margin-right: 0 !important;
font-size: .92em !important;
}
.httpdns-note.comment {
}
.httpdns-note.comment {
color: #8f9aa6 !important;
font-size: 12px;
margin-top: .45em !important;
}
}
.httpdns-auth-table td.title {
width: 260px !important;
min-width: 260px !important;
white-space: nowrap;
word-break: keep-all;
}
.httpdns-secret-line {
display: flex;
align-items: center;
gap: .35em;
}
.httpdns-mini-icon {
color: #8c96a3 !important;
font-size: 12px;
line-height: 1;
}
.httpdns-mini-icon:hover {
color: #5e6c7b !important;
}
.httpdns-state-on {
color: #21ba45;
font-weight: 600;
}
.httpdns-state-off {
color: #8f9aa6;
font-weight: 600;
}
</style>
<div>
<div class="ui menu text blue">
<div class="item"><strong>{{app.name}}</strong> (<code>{{settings.appId}}</code>)</div>
</div>
<div class="ui divider"></div>
<div class="right-box with-menu">
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<csrf-token></csrf-token>
<input type="hidden" name="appId" :value="app.id" />
<div class="ui stackable grid httpdns-settings-grid">
<div class="three wide computer four wide tablet sixteen wide mobile column left-nav-column">
<div class="ui fluid vertical pointing menu httpdns-side-menu">
<a href="" class="item" :class="{active: activeSection == 'basic'}" @click.prevent="activeSection='basic'">基础配置</a>
<a href="" class="item" :class="{active: activeSection == 'auth'}" @click.prevent="activeSection='auth'">认证与密钥</a>
</div>
</div>
<div class="thirteen wide computer twelve wide tablet sixteen wide mobile column right-form-column">
<table class="ui table selectable definition" v-show="activeSection == 'basic'">
<tr>
<td class="title">App ID</td>
@@ -62,9 +72,10 @@
</tr>
<tr>
<td class="title">应用启用</td>
<td><checkbox name="appStatus" value="1" v-model="settings.appStatus"></checkbox></td>
<td>
<checkbox name="appStatus" value="1" v-model="settings.appStatus"></checkbox>
</td>
</tr>
<tr>
<td class="title">SNI 防护配置</td>
<td>
@@ -74,35 +85,51 @@
</tr>
</table>
<table class="ui table selectable definition" v-show="activeSection == 'auth'">
<table class="ui table selectable definition httpdns-auth-table" v-show="activeSection == 'auth'">
<tr>
<td class="title">请求验签</td>
<td>
<span class="ui label tiny green" v-if="settings.signEnabled">已开启</span>
<span class="ui label tiny basic" v-else>已关闭</span>
<a href="" class="ui mini button" :class="settings.signEnabled ? 'basic' : 'primary'" style="margin-left: .8em;" @click.prevent="toggleSignEnabled">{{settings.signEnabled ? "关闭请求验签" : "开启请求验签"}}</a>
<span
:class="settings.signEnabled ? 'httpdns-state-on' : 'httpdns-state-off'">{{settings.signEnabled
? "已开启" : "已关闭"}}</span>
<a href="" class="ui mini button basic" style="margin-left: .8em;"
@click.prevent="toggleSignEnabled">{{settings.signEnabled ? "关闭请求验签" : "开启请求验签"}}</a>
<p class="comment httpdns-note">打开后,服务端会对请求进行签名校验。</p>
</td>
</tr>
<tr>
<td class="title">加签 Secret</td>
<td>
<div class="httpdns-secret-line">
<code>{{signSecretVisible ? settings.signSecretPlain : settings.signSecretMasked}}</code>
<a href="" class="httpdns-mini-action" @click.prevent="signSecretVisible = !signSecretVisible" :title="signSecretVisible ? '隐藏明文' : '查看明文'"><i class="icon" :class="signSecretVisible ? 'eye slash' : 'eye'"></i></a>
<a href="" class="httpdns-mini-action" title="复制加签 Secret" @click.prevent="copySecret(settings.signSecretPlain, '加签 Secret')"><i class="copy outline icon"></i></a>
<a href="" class="httpdns-mini-action" @click.prevent="resetSignSecret">[重置]</a>
<a href="" class="httpdns-mini-icon" @click.prevent="signSecretVisible = !signSecretVisible"
:title="signSecretVisible ? '隐藏明文' : '查看明文'"><i class="icon"
:class="signSecretVisible ? 'eye slash' : 'eye'"></i></a>
<a href="" class="httpdns-mini-icon" title="复制加签 Secret"
@click.prevent="copySecret(settings.signSecretPlain, '加签 Secret')"><i
class="copy outline icon"></i></a>
<a href="" class="httpdns-mini-icon" title="重置加签 Secret" @click.prevent="resetSignSecret"><i
class="redo icon"></i></a>
</div>
<p class="comment httpdns-note">最近更新:{{settings.signSecretUpdatedAt}}</p>
<p class="comment httpdns-note" v-if="!settings.signEnabled">请求验签已关闭,当前不使用加签 Secret。</p>
<p class="comment httpdns-note">用于生成鉴权接口的安全密钥。</p>
</td>
</tr>
<tr>
<td class="title">AES 数据加密 Secret</td>
<td class="title">数据加密 Secret</td>
<td>
<div class="httpdns-secret-line">
<code>{{aesSecretVisible ? settings.aesSecretPlain : settings.aesSecretMasked}}</code>
<a href="" class="httpdns-mini-action" @click.prevent="aesSecretVisible = !aesSecretVisible" :title="aesSecretVisible ? '隐藏明文' : '查看明文'"><i class="icon" :class="aesSecretVisible ? 'eye slash' : 'eye'"></i></a>
<a href="" class="httpdns-mini-action" title="复制 AES 数据加密 Secret" @click.prevent="copySecret(settings.aesSecretPlain, 'AES Secret')"><i class="copy outline icon"></i></a>
<a href="" class="httpdns-mini-action" @click.prevent="resetAESSecret">[重置]</a>
<a href="" class="httpdns-mini-icon" @click.prevent="aesSecretVisible = !aesSecretVisible"
:title="aesSecretVisible ? '隐藏明文' : '查看明文'"><i class="icon"
:class="aesSecretVisible ? 'eye slash' : 'eye'"></i></a>
<a href="" class="httpdns-mini-icon" title="复制数据加密 Secret"
@click.prevent="copySecret(settings.aesSecretPlain, 'AES Secret')"><i
class="copy outline icon"></i></a>
<a href="" class="httpdns-mini-icon" title="重置数据加密 Secret" @click.prevent="resetAESSecret"><i
class="redo icon"></i></a>
</div>
<p class="comment httpdns-note">最近更新:{{settings.aesSecretUpdatedAt}}</p>
<p class="comment httpdns-note">用于解析接口数据加密的密钥。</p>
</td>
@@ -110,7 +137,5 @@
</table>
<submit-btn></submit-btn>
</div>
</div>
</form>
</div>

View File

@@ -1,5 +1,5 @@
Tea.context(function () {
this.activeSection = "basic";
this.activeSection = this.activeSection || "basic";
this.success = NotifyReloadSuccess("保存成功");
this.signSecretVisible = false;
this.aesSecretVisible = false;
@@ -9,7 +9,7 @@ Tea.context(function () {
let targetIsOn = !this.settings.signEnabled;
if (targetIsOn) {
teaweb.confirm("html:开启后,服务端会对解析请求进行签鉴权,<span class='red'>未签名、签名无效或过期的请求都解析失败</span>,确认开启吗?", function () {
teaweb.confirm("html:开启后,服务端会对解析请求进行签鉴权,<span class='red'>未签名、签名无效或过期的请求都解析失败</span>,确认开启吗?", function () {
that.$post("/httpdns/apps/app/settings/toggleSignEnabled")
.params({
appId: that.app.id,
@@ -23,7 +23,7 @@ Tea.context(function () {
return;
}
teaweb.confirm("html:关闭后,服务端不会对解析请求进行签鉴权,可能<span class='red'>存在被刷风险</span>,确认关闭吗?", function () {
teaweb.confirm("html:关闭后,服务端不会对解析请求进行签鉴权,可能<span class='red'>存在被刷风险</span>,确认关闭吗?", function () {
that.$post("/httpdns/apps/app/settings/toggleSignEnabled")
.params({
appId: that.app.id,
@@ -109,3 +109,4 @@ Tea.context(function () {
});
};
});

View File

@@ -13,13 +13,23 @@
</td>
</tr>
<tr>
<td>所属集群 *</td>
<td>集群 *</td>
<td>
<select class="ui dropdown" name="clusterId" v-model="defaultClusterId">
<select class="ui dropdown" name="primaryClusterId" v-model="defaultPrimaryClusterId">
<option value="">[请选择集群]</option>
<option v-for="cluster in clusters" :value="cluster.id">{{cluster.name}}</option>
</select>
<p class="comment">应用解析请求将由所选集群下的网关节点处理。默认值来自“HTTPDNS 用户设置”</p>
<p class="comment">主集群用于优先处理应用解析请求。</p>
</td>
</tr>
<tr>
<td>备集群</td>
<td>
<select class="ui dropdown" name="backupClusterId" v-model="defaultBackupClusterId">
<option :value="0">[不设置]</option>
<option v-for="cluster in clusters" :value="cluster.id">{{cluster.name}}</option>
</select>
<p class="comment">当主集群不可用时,可切换到备集群。</p>
</td>
</tr>
<tr>

View File

@@ -1,38 +1,31 @@
{$layout}
{$template "menu"}
{$layout}
<style>
.httpdns-col-ttl {
width: 72px;
white-space: nowrap;
}
.httpdns-col-actions {
width: 130px;
white-space: nowrap;
}
</style>
<div>
<div class="ui menu text blue">
<div class="item"><strong>{{app.name}}</strong> (<code>{{app.appId}}</code>)</div>
</div>
<div class="ui divider"></div>
<second-menu>
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">{{app.name}}</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">域名列表</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<div class="item"><strong>{{domain.name}}</strong></div>
<a href="" class="item" @click.prevent="createRecord" :class="{disabled: !domain || !domain.id}">创建规则</a>
</second-menu>
<div class="ui small message">
当前域名:<strong>{{domain.name}}</strong>
</div>
<a href="" @click.prevent="createRecord" class="ui button primary small" :class="{disabled: !domain || !domain.id}">
<i class="icon plus"></i> 新增自定义解析规则
</a>
<a :href="'/httpdns/apps/domains?appId=' + app.id" class="ui button small">返回域名管理</a>
<table class="ui table selectable celled" v-if="records.length > 0" style="margin-top: 1em;">
<table class="ui table selectable celled" v-if="records.length > 0" style="margin-top: .8em;">
<thead>
<tr>
<th>线路</th>
<th>规则名称</th>
<th>SDNS 参数</th>
<th>解析记录</th>
<th class="httpdns-col-ttl">TTL</th>
<th class="width10">状态</th>
@@ -43,10 +36,7 @@
<tr v-for="record in records">
<td>{{record.lineText}}</td>
<td>{{record.ruleName}}</td>
<td>{{record.paramsText}}</td>
<td>
{{record.recordValueText}}
</td>
<td>{{record.recordValueText}}</td>
<td class="httpdns-col-ttl">{{record.ttl}}s</td>
<td>
<span class="green" v-if="record.isOn">已启用</span>
@@ -59,8 +49,7 @@
</td>
</tr>
</tbody>
</table>
</table>
<p class="ui warning message" v-if="!domain || !domain.id">当前应用暂无可用域名,请先到域名管理中添加域名。</p>
<p class="ui message" v-else-if="records.length == 0">当前域名还没有自定义解析规则。</p>
</div>
<not-found-box v-if="!domain || !domain.id">当前应用暂无可用域名,请先到域名管理中添加域名。</not-found-box>
<not-found-box v-else-if="records.length == 0">当前域名还没有自定义解析规则。</not-found-box>

View File

@@ -1,4 +1,4 @@
Tea.context(function () {
Tea.context(function () {
if (typeof this.records == "undefined") {
this.records = [];
}

View File

@@ -38,7 +38,6 @@
<input type="hidden" name="domainId" :value="domain.id" />
<input type="hidden" name="domain" :value="record.domain" />
<input type="hidden" name="recordId" :value="record.id" />
<input type="hidden" name="sdnsParamsJSON" :value="JSON.stringify(sdnsParams)" />
<input type="hidden" name="recordItemsJSON" :value="JSON.stringify(recordItems)" />
<table class="ui table definition selectable">
@@ -81,26 +80,6 @@
</div>
</td>
</tr>
<tr>
<td class="title">SDNS 参数配置</td>
<td>
<div class="httpdns-row" v-for="(param, index) in sdnsParams">
<div class="field flex">
<input type="text" maxlength="64" placeholder="参数名称" v-model="param.name" />
</div>
<div class="field flex">
<input type="text" maxlength="64" placeholder="参数值" v-model="param.value" />
</div>
<a href="" @click.prevent="removeSDNSParam(index)" title="删除"><i class="icon trash alternate outline"></i></a>
</div>
<div class="httpdns-inline-actions">
<a href="" @click.prevent="addSDNSParam" :class="{disabled: sdnsParams.length >= 10}">
<i class="icon plus circle"></i>添加参数
</a>
<span class="count">{{sdnsParams.length}}/10</span>
</div>
</td>
</tr>
<tr>
<td class="title">解析记录值 *</td>
<td>

View File

@@ -75,11 +75,6 @@ Tea.context(function () {
vm.record.weightEnabled = vm.normalizeBoolean(vm.record.weightEnabled, false);
vm.record.isOn = vm.normalizeBoolean(vm.record.isOn, true);
vm.sdnsParams = vm.parseJSONList(vm.record.sdnsParamsJson);
if (vm.sdnsParams.length == 0) {
vm.sdnsParams.push({name: "", value: ""});
}
vm.recordItems = vm.parseJSONList(vm.record.recordItemsJson);
if (vm.recordItems.length == 0) {
vm.recordItems.push({type: "A", value: "", weight: 100});
@@ -139,23 +134,6 @@ Tea.context(function () {
}
};
vm.addSDNSParam = function () {
if (vm.sdnsParams.length >= 10) {
return;
}
vm.sdnsParams.push({name: "", value: ""});
};
vm.removeSDNSParam = function (index) {
if (index < 0 || index >= vm.sdnsParams.length) {
return;
}
vm.sdnsParams.splice(index, 1);
if (vm.sdnsParams.length == 0) {
vm.sdnsParams.push({name: "", value: ""});
}
};
vm.addRecordItem = function () {
if (vm.recordItems.length >= 10) {
return;

View File

@@ -0,0 +1,12 @@
{$layout}
<second-menu>
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">{{app.name}}</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<div class="item"><strong>删除应用</strong></div>
</second-menu>
<div class="buttons-box">
<button class="ui button red" type="button" @click.prevent="deleteApp(app.id)">删除当前应用</button>
<p class="comment" style="margin-top: .8em;">包含{{domainCount}}域名</p>
</div>

View File

@@ -0,0 +1,16 @@
Tea.context(function () {
this.deleteApp = function (appId) {
let that = this;
teaweb.confirm("确定要删除此应用吗?", function () {
that.$post("/httpdns/apps/delete")
.params({
appId: appId
})
.success(function () {
teaweb.success("删除成功", function () {
window.location = "/httpdns/apps";
});
});
});
};
});

View File

@@ -1,35 +1,62 @@
{$layout}
{$template "menu"}
{$layout}
<div>
<div class="ui menu text blue">
<div class="item"><strong>{{app.name}}</strong> (<code>{{app.appId}}</code>)</div>
<div class="margin"></div>
<style>
.httpdns-domains-table .httpdns-domains-op {
white-space: nowrap;
}
</style>
<second-menu>
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">{{app.name}}</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<div class="item"><strong>域名列表</strong></div>
<a href="" class="item" @click.prevent="bindDomain">创建域名</a>
</second-menu>
<form class="ui form small" @submit.prevent="doSearch">
<div class="ui fields inline">
<div class="ui field">
<input type="text" v-model.trim="keywordInput" placeholder="按域名筛选..." />
</div>
<div class="ui divider"></div>
<div class="ui field">
<button type="submit" class="ui button small">搜索</button>
&nbsp;
<a href="" v-if="keyword.length > 0" @click.prevent="clearSearch">[清除条件]</a>
</div>
</div>
</form>
<a href="" @click.prevent="bindDomain" class="ui button primary small"><i class="icon plus"></i>添加域名</a>
<table class="ui table selectable celled" v-if="domains.length > 0" style="margin-top: 1em;">
<table class="ui table selectable celled httpdns-domains-table" v-if="filteredDomains().length > 0">
<colgroup>
<col style="width:60%;" />
<col style="width:20%;" />
<col style="width:20%;" />
</colgroup>
<thead>
<tr>
<th>域名列表</th>
<th class="two wide">规则策略</th>
<th class="two op">操作</th>
<th>服务域名</th>
<th class="width10">规则策略</th>
<th class="httpdns-domains-op">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="domain in domains">
<td><strong>{{domain.name}}</strong></td>
<tr v-for="domain in filteredDomains()">
<td>
<a :href="'/httpdns/apps/customRecords?appId=' + app.id + '&domainId=' + domain.id">{{domain.customRecordCount}}</a>
<div><strong>{{domain.name}}</strong></div>
</td>
<td>
<a :href="'/httpdns/apps/customRecords?appId=' + app.id + '&domainId=' + domain.id">自定义解析</a> &nbsp;|&nbsp;
<a
:href="'/httpdns/apps/customRecords?appId=' + app.id + '&domainId=' + domain.id">{{domain.customRecordCount}}</a>
</td>
<td class="httpdns-domains-op">
<a :href="'/httpdns/apps/customRecords?appId=' + app.id + '&domainId=' + domain.id">自定义解析</a>
&nbsp;|&nbsp;
<a href="" @click.prevent="deleteDomain(domain.id)">解绑</a>
</td>
</tr>
</tbody>
</table>
</table>
<p class="ui message" v-if="domains.length == 0">应用尚未绑定域名。</p>
</div>
<not-found-box v-if="domains.length == 0">当前应用尚未绑定域名。</not-found-box>
<not-found-box v-if="domains.length > 0 && filteredDomains().length == 0">没有匹配的域名。</not-found-box>

View File

@@ -1,8 +1,37 @@
Tea.context(function () {
Tea.context(function () {
if (typeof this.keywordInput == "undefined") {
this.keywordInput = "";
}
if (typeof this.keyword == "undefined") {
this.keyword = "";
}
if (typeof this.domains == "undefined") {
this.domains = [];
}
this.keywordInput = this.keyword;
this.doSearch = function () {
this.keyword = this.keywordInput.trim();
};
this.clearSearch = function () {
this.keywordInput = "";
this.keyword = "";
};
this.filteredDomains = function () {
let keyword = this.keyword.trim().toLowerCase();
if (keyword.length == 0) {
return this.domains;
}
return this.domains.filter(function (domain) {
let name = (domain.name || "").toLowerCase();
return name.indexOf(keyword) >= 0;
});
};
this.bindDomain = function () {
teaweb.popup("/httpdns/apps/domains/createPopup?appId=" + this.app.id, {
height: "24em",
@@ -11,6 +40,19 @@ Tea.context(function () {
});
};
this.deleteApp = function () {
let that = this;
teaweb.confirm("确定要删除当前应用吗?", function () {
that.$post("/httpdns/apps/delete")
.params({
appId: that.app.id
})
.success(function () {
window.location = "/httpdns/apps";
});
});
};
this.deleteDomain = function (domainId) {
let that = this;
teaweb.confirm("确定要解绑这个域名吗?", function () {

View File

@@ -1,6 +1,14 @@
{$layout}
{$template "menu"}
<div class="ui margin"></div>
<style>
.httpdns-apps-table .httpdns-apps-op {
white-space: nowrap;
}
</style>
<div>
<form method="get" class="ui form" action="/httpdns/apps">
<div class="ui fields inline">
@@ -16,21 +24,30 @@
<p class="ui message" v-if="apps.length == 0">暂时没有符合条件的 HTTPDNS 应用。</p>
<table class="ui table selectable celled" v-if="apps.length > 0">
<table class="ui table selectable celled httpdns-apps-table" v-if="apps.length > 0">
<colgroup>
<col style="width:34%;" />
<col style="width:28%;" />
<col style="width:12%;" />
<col style="width:10%;" />
<col style="width:16%;" />
</colgroup>
<thead>
<tr>
<th>应用名称</th>
<th>AppID</th>
<th>绑定域名数</th>
<th class="one wide center">状态</th>
<th class="tz op">操作</th>
<th class="center">绑定域名数</th>
<th class="center">状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="app in apps">
<td>
<a :href="'/httpdns/apps/app?appId=' + app.id">
<strong><keyword :v-word="keyword">{{app.name}}</keyword></strong>
<strong>
<keyword :v-word="keyword">{{app.name}}</keyword>
</strong>
</a>
</td>
<td>
@@ -41,9 +58,10 @@
<td class="center">
<label-on :v-is-on="app.isOn"></label-on>
</td>
<td>
<td class="httpdns-apps-op">
<a :href="'/httpdns/apps/app?appId=' + app.id">域名管理</a> &nbsp;|&nbsp;
<a :href="'/httpdns/apps/app/settings?appId=' + app.id">应用设置</a>
<a :href="'/httpdns/apps/app/settings?appId=' + app.id">应用设置</a> &nbsp;|&nbsp;
<a :href="'/httpdns/apps/sdk?appId=' + app.id">SDK集成</a>
</td>
</tr>
</tbody>

View File

@@ -0,0 +1,79 @@
{$layout}
<style>
.httpdns-sdk-cards {
margin-top: 1em;
}
.httpdns-sdk-cards > .card {
min-height: 168px;
}
.httpdns-sdk-desc {
margin-top: .6em;
color: #5e6c7b;
}
.httpdns-sdk-meta {
margin-top: .35em;
color: #9aa6b2;
font-size: 12px;
}
.httpdns-sdk-actions {
display: inline-flex;
align-items: center;
gap: .6em;
flex-wrap: wrap;
}
.httpdns-sdk-actions .button {
padding-left: .8em !important;
padding-right: .8em !important;
}
</style>
<second-menu>
<a class="item" :href="'/httpdns/apps/domains?appId=' + app.id">{{app.name}}</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<div class="item"><strong>SDK 集成</strong></div>
</second-menu>
<div class="ui three stackable cards httpdns-sdk-cards">
<div class="card">
<div class="content">
<div class="header"><i class="icon android green"></i> Android SDK</div>
<div class="httpdns-sdk-meta">Java / Kotlin</div>
<div class="description httpdns-sdk-desc">适用于 Android 客户端接入。</div>
</div>
<div class="extra content">
<div class="httpdns-sdk-actions">
<a class="ui button compact mini basic" href="javascript:;"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="javascript:;"><i class="icon book"></i> 集成文档</a>
</div>
</div>
</div>
<div class="card">
<div class="content">
<div class="header"><i class="icon apple grey"></i> iOS SDK</div>
<div class="httpdns-sdk-meta">Objective-C / Swift</div>
<div class="description httpdns-sdk-desc">适用于 iOS 客户端接入。</div>
</div>
<div class="extra content">
<div class="httpdns-sdk-actions">
<a class="ui button compact mini basic" href="javascript:;"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="javascript:;"><i class="icon book"></i> 集成文档</a>
</div>
</div>
</div>
<div class="card">
<div class="content">
<div class="header"><i class="icon mobile alternate blue"></i> Flutter SDK</div>
<div class="httpdns-sdk-meta">Dart / Plugin</div>
<div class="description httpdns-sdk-desc">适用于 Flutter 跨平台接入。</div>
</div>
<div class="extra content">
<div class="httpdns-sdk-actions">
<a class="ui button compact mini basic" href="javascript:;"><i class="icon download"></i> 下载 SDK</a>
<a class="ui button compact mini basic" href="javascript:;"><i class="icon book"></i> 集成文档</a>
</div>
</div>
</div>
</div>

View File

@@ -1,8 +1,4 @@
<second-menu>
<menu-item :href="'/httpdns/clusters/cluster?clusterId=' + cluster.id">{{cluster.name}}</menu-item>
<second-menu>
<a class="item" :href="'/httpdns/clusters/cluster?clusterId=' + cluster.id">{{cluster.name}}</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<menu-item :href="'/httpdns/clusters/cluster?clusterId=' + cluster.id" code="index">节点列表</menu-item>
<menu-item :href="'/httpdns/clusters/createNode?clusterId=' + cluster.id" code="createNode">创建节点</menu-item>
<menu-item :href="'/httpdns/clusters/cluster/settings?clusterId=' + cluster.id" code="settings">集群设置</menu-item>
<menu-item :href="'/httpdns/clusters/delete?clusterId=' + cluster.id" code="delete">删除集群</menu-item>
</second-menu>

View File

@@ -1,10 +1,11 @@
{$layout}
{$template "cluster_menu"}
<div class="ui margin"></div>
<div class="right-menu">
<a :href="'/httpdns/clusters/createNode?clusterId=' + clusterId" class="item">[创建节点]</a>
</div>
<second-menu>
<a class="item" :href="'/httpdns/clusters/cluster?clusterId=' + cluster.id">{{cluster.name}}</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<div class="item"><strong>节点列表</strong></div>
<a class="item" :href="'/httpdns/clusters/createNode?clusterId=' + clusterId">创建节点</a>
</second-menu>
<form class="ui form" action="/httpdns/clusters/cluster">
<input type="hidden" name="clusterId" :value="clusterId" />
@@ -43,6 +44,9 @@
<tr>
<th>节点名称</th>
<th>IP地址</th>
<th class="center">CPU</th>
<th class="center">内存</th>
<th class="center">负载</th>
<th class="width-5 center">状态</th>
<th class="two op">操作</th>
</tr>
@@ -64,11 +68,21 @@
<div class="ui label tiny basic">{{addr.ip}}
<span class="small" v-if="addr.name.length > 0">{{addr.name}}</span>
<span class="small red" v-if="!addr.canAccess" title="不公开访问">[内]</span>
<span class="small red" v-if="!addr.isOn">[下线]</span>
<span class="small red" v-if="!addr.isUp" title="健康检查失败">[宕机]</span>
</div>
</div>
</td>
<td class="center">
<span v-if="node.status != null && node.status.isActive">{{node.status.cpuUsageText}}</span>
<span v-else class="disabled">-</span>
</td>
<td class="center">
<span v-if="node.status != null && node.status.isActive">{{node.status.memUsageText}}</span>
<span v-else class="disabled">-</span>
</td>
<td class="center">
<span v-if="node.status != null && node.status.isActive && node.status.load1m > 0">{{node.status.load1m}}</span>
<span v-else class="disabled">-</span>
</td>
<td class="center">
<div v-if="!node.isUp">
<span class="red">宕机</span>

View File

@@ -1,31 +1,57 @@
{$layout}
{$template "cluster_menu"}
<div class="ui margin"></div>
<h3 class="ui header">集群设置</h3>
<p><strong>{{cluster.name}}</strong></p>
{$var "header"}
<script src="/servers/certs/datajs" type="text/javascript"></script>
<script src="/js/sortable.min.js" type="text/javascript"></script>
{$end}
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<second-menu>
<a class="item" :href="'/httpdns/clusters/cluster?clusterId=' + cluster.id">{{cluster.name}}</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<div class="item"><strong>集群设置</strong></div>
</second-menu>
{$template "/left_menu_with_menu"}
<div class="right-box with-menu">
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<input type="hidden" name="clusterId" :value="cluster.id" />
<table class="ui table definition selectable">
<table class="ui table definition selectable" v-show="activeSection == 'basic'">
<tr>
<td class="title">集群名称 *</td>
<td>
<input type="text" name="name" maxlength="50" ref="focus" v-model="cluster.name" />
<p class="comment">用于区分不同环境的解析节点池</p>
<p class="comment">用于在系统内部标识该 HTTPDNS 集群</p>
</td>
</tr>
<tr>
<td>集群服务域名 *</td>
<td>服务域名 *</td>
<td>
<input type="text" name="gatewayDomain" maxlength="255" v-model="settings.gatewayDomain" placeholder="例如 gw-hz.httpdns.example.com" />
<p class="comment">该集群下应用用于 SDK 接入的 HTTPDNS 服务域名。</p>
<input type="text" name="gatewayDomain" maxlength="255" v-model="settings.gatewayDomain"
placeholder="例如 gw-hz.httpdns.example.com" />
<p class="comment">当前集群对外提供 HTTPDNS 服务的接入域名。</p>
</td>
</tr>
<tr>
<td colspan="2"><more-options-indicator></more-options-indicator></td>
<td>节点安装根目录</td>
<td>
<input type="text" name="installDir" maxlength="100" v-model="settings.installDir" />
<p class="comment">边缘节点安装 HTTPDNS 服务的默认所在目录,此目录将被用于下发配置。通常保持默认即可。</p>
</td>
</tr>
<tr>
<td>默认解析 TTL</td>
<td>
<div class="ui input right labeled">
<input type="text" name="cacheTtl" maxlength="5" v-model="settings.cacheTtl"
style="width: 6em" />
<span class="ui label"></span>
</div>
<p class="comment">SDK 向 HTTPDNS 请求域名解析时,返回的默认缓存有效期 (TTL)。SDK 超时后将重新发起请求。</p>
</td>
</tr>
<tbody v-show="moreOptionsVisible">
<tr>
<td>降级超时容忍度</td>
<td>
@@ -34,38 +60,46 @@
style="width: 6em" />
<span class="ui label">毫秒</span>
</div>
<p class="comment">HTTPDNS 网关请求源站超时多长时间后强制降级返回缓存兜底IP保障 P99 响应延迟)。</p>
</td>
</tr>
<tr>
<td>本地内存缓存</td>
<td>
<div class="ui input right labeled">
<input type="text" name="cacheTtl" maxlength="5" v-model="settings.cacheTtl"
style="width: 6em" />
<span class="ui label"></span>
</div>
<p class="comment">在网关节点内存中缓存解析结果的时长,缓解峰值查询压力。</p>
</td>
</tr>
<tr>
<td>节点安装根目录</td>
<td>
<input type="text" name="installDir" maxlength="100" v-model="settings.installDir" />
<p class="comment">此集群下新加网关节点的主程序默认部署路径。</p>
<p class="comment">HTTPDNS 节点在回源查询其它 DNS 时的最大等待时间。超出此时间将触发服务降级逻辑(返回上一有效缓存或错误)。</p>
</td>
</tr>
<tr>
<td>启用当前集群</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="isOn" value="1" v-model="settings.isOn" />
<input type="checkbox" name="isOn" value="1" v-model="settings.isOn"
@change="syncDefaultCluster" />
<label></label>
</div>
<p class="comment">如果取消启用,此集群下的所有 HTTPDNS 网关节点将停止处理解析请求</p>
<p class="comment">如果取消启用,整个集群的 HTTPDNS 解析服务将停止</p>
</td>
</tr>
<tr>
<td>默认集群</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="isDefaultCluster" value="1" v-model="settings.isDefaultCluster" />
<label>设置为默认部署集群</label>
</div>
<p class="comment">全局设置。如果应用未单独指定集群,将默认分配和部署到该集群中。</p>
</td>
</tr>
</tbody>
</table>
<table class="ui table selectable definition" v-show="activeSection == 'tls'">
<tr>
<td class="title">绑定端口 *</td>
<td>
<network-addresses-box :v-url="'/httpdns/addPortPopup'" :v-addresses="tlsConfig.listen"
:v-protocol="'tls'" :v-support-range="true"></network-addresses-box>
</td>
</tr>
</table>
<!-- SSL配置 -->
<ssl-config-box v-show="activeSection == 'tls'" :v-ssl-policy="tlsConfig.sslPolicy"
:v-protocol="'tls'"></ssl-config-box>
<submit-btn></submit-btn>
</form>
</form>
</div>

View File

@@ -1,3 +1,11 @@
Tea.context(function () {
Tea.context(function () {
this.success = NotifyReloadSuccess("保存成功");
this.activeSection = this.activeSection || "basic";
this.tlsAdvancedVisible = false;
if (!this.settings) {
this.settings = {};
}
});

View File

@@ -1,5 +1,10 @@
{$layout}
{$template "cluster_menu"}
<second-menu>
<a class="item" :href="'/httpdns/clusters/cluster?clusterId=' + cluster.id">{{cluster.name}}</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<div class="item"><strong>创建节点</strong></div>
</second-menu>
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<input type="hidden" name="clusterId" :value="clusterId" />

View File

@@ -1,6 +1,10 @@
{$layout}
{$template "cluster_menu"}
<div class="ui margin"></div>
<second-menu>
<a class="item" :href="'/httpdns/clusters/cluster?clusterId=' + cluster.id">{{cluster.name}}</a>
<span class="item disabled" style="padding-left: 0; padding-right: 0">&raquo;</span>
<div class="item"><strong>删除集群</strong></div>
</second-menu>
<div class="buttons-box">
<button class="ui button red" type="button" @click.prevent="deleteCluster(cluster.id)">删除当前集群</button>

View File

@@ -1,6 +1,8 @@
{$layout}
{$template "menu"}
<div class="ui margin"></div>
<div>
<div v-if="hasErrorLogs" class="ui icon message small error">
<i class="icon warning circle"></i>
@@ -66,3 +68,4 @@
<div class="page" v-html="page"></div>
</div>

View File

@@ -74,8 +74,9 @@
<tr>
<td>集群服务地址</td>
<td>
<code>{{selectedApp.gatewayDomain}}</code>
<a href="" class="httpdns-mini-action" title="复制服务地址" @click.prevent="copyText(selectedApp.gatewayDomain, '服务地址')"><i class="copy outline icon"></i></a>
<code>{{selectedApp.gatewayDomainDisplay}}</code>
<a href="" class="httpdns-mini-action" title="复制服务地址" @click.prevent="copyText(selectedApp.gatewayDomainDisplay, '服务地址')"><i class="copy outline icon"></i></a>
<p class="comment" v-if="selectedApp.gatewayDomains && selectedApp.gatewayDomains.length > 1">已启用主备:第一个为主集群,后续为备集群。</p>
</td>
</tr>
<tr>

View File

@@ -24,6 +24,9 @@
if (!this.selectedApp.gatewayDomain || this.selectedApp.gatewayDomain.length == 0) {
this.selectedApp.gatewayDomain = "gw.httpdns.example.com"
}
if (!this.selectedApp.gatewayDomainDisplay || this.selectedApp.gatewayDomainDisplay.length == 0) {
this.selectedApp.gatewayDomainDisplay = this.selectedApp.gatewayDomain
}
this.signSecretVisible = false
this.aesSecretVisible = false

View File

@@ -72,13 +72,6 @@
<p class="comment httpdns-policy-note">用户新建应用时默认落到此集群。</p>
</td>
</tr>
<tr>
<td class="title">启用用户域名校验</td>
<td>
<checkbox name="enableUserDomainVerify" value="1" v-model="policies.enableUserDomainVerify"></checkbox>
<p class="comment httpdns-policy-note">开启后,用户添加域名需要通过归属校验。</p>
</td>
</tr>
</table>
<table class="ui table definition selectable" v-show="activeSection == 'basic'">

View File

@@ -10,9 +10,6 @@ Tea.context(function () {
this.policies.defaultClusterId = this.availableClusters[0].id;
}
}
if (typeof this.policies.enableUserDomainVerify == "undefined") {
this.policies.enableUserDomainVerify = true;
}
if (typeof this.policies.defaultTTL == "undefined" || this.policies.defaultTTL <= 0) {
this.policies.defaultTTL = 30;
}

View File

@@ -1,3 +1,3 @@
<first-menu>
<menu-item href="/httpdns/resolveLogs" code="index">解析日志</menu-item>
<menu-item href="/httpdns/resolveLogs" code="index">访问日志</menu-item>
</first-menu>

View File

@@ -1,20 +1,22 @@
{$layout}
{$template "menu"}
<div class="margin"></div>
<style>
.httpdns-log-summary {
.httpdns-log-summary {
color: #556070;
font-size: 12px;
line-height: 1.6;
white-space: nowrap;
}
}
.httpdns-log-summary code {
.httpdns-log-summary code {
font-size: 12px;
}
}
</style>
<form method="get" action="/httpdns/resolveLogs" class="ui form" autocomplete="off">
<div>
<form method="get" action="/httpdns/resolveLogs" class="ui form small" autocomplete="off">
<div class="ui fields inline">
<div class="ui field">
<select class="ui dropdown" name="clusterId" v-model="clusterId">
@@ -25,14 +27,15 @@
<div class="ui field">
<select class="ui dropdown" name="nodeId" v-model="nodeId">
<option value="">[节点]</option>
<option v-for="node in nodes" :value="node.id" v-if="clusterId == '' || clusterId == node.clusterId">{{node.name}}</option>
<option v-for="node in nodes" :value="node.id"
v-if="clusterId == '' || clusterId == node.clusterId">{{node.name}}</option>
</select>
</div>
<div class="ui field">
<input type="text" name="appId" v-model="appId" placeholder="AppID" style="width:10em" />
<input type="text" name="appId" v-model="appId" placeholder="AppID" />
</div>
<div class="ui field">
<input type="text" name="domain" v-model="domain" placeholder="域名" style="width:13em" />
<input type="text" name="domain" v-model="domain" placeholder="域名" />
</div>
<div class="ui field">
<select class="ui dropdown" name="status" v-model="status">
@@ -42,20 +45,25 @@
</select>
</div>
<div class="ui field">
<input type="text" name="keyword" v-model="keyword" placeholder="应用/域名/IP/结果IP" style="width:14em" />
<input type="text" name="keyword" v-model="keyword" placeholder="应用/域名/IP/结果IP" />
</div>
<div class="ui field">
<button type="submit" class="ui button">查询</button>
<button type="submit" class="ui button small">查询</button>
</div>
<div class="ui field" v-if="clusterId.toString().length > 0 || nodeId.toString().length > 0 || appId.length > 0 || domain.length > 0 || status.length > 0 || keyword.length > 0">
<div class="ui field"
v-if="clusterId.toString().length > 0 || nodeId.toString().length > 0 || appId.length > 0 || domain.length > 0 || status.length > 0 || keyword.length > 0">
<a href="/httpdns/resolveLogs">[清除条件]</a>
</div>
</div>
</form>
</form>
</div>
<p class="comment" v-if="resolveLogs.length == 0">暂时还没有解析日志。</p>
<div class="margin"></div>
<div style="overflow-x:auto;" v-if="resolveLogs.length > 0">
<not-found-box v-if="resolveLogs.length == 0">暂时还没有访问日志。</not-found-box>
<div v-if="resolveLogs.length > 0">
<div style="overflow-x:auto;">
<table class="ui table selectable celled">
<thead>
<tr>
@@ -75,7 +83,7 @@
<td>
<div class="httpdns-log-summary">
{{log.time}}
| {{log.appName}}<code>{{log.appId}}</code>
| {{log.appName}} (<code>{{log.appId}}</code>)
| <code>{{log.clientIp}}</code>
| {{log.os}}/{{log.sdkVersion}}
| {{log.query}} {{log.domain}} ->
@@ -84,11 +92,12 @@
|
<span class="green" v-if="log.status == 'success'"><strong>成功</strong></span>
<span class="red" v-else><strong>失败</strong></span>
<span class="grey" v-if="log.errorCode != 'none'">{{log.errorCode}}</span>
<span class="grey" v-if="log.errorCode != 'none'">({{log.errorCode}})</span>
| {{log.costMs}}ms
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -1,8 +1,16 @@
{$layout}
{$template "menu"}
<div class="margin"></div>
{$template "/datepicker"}
<form method="get" action="/httpdns/runtimeLogs" class="ui form" autocomplete="off">
<style>
.httpdns-runtime-level {
font-weight: 600;
}
</style>
<form method="get" action="/httpdns/runtimeLogs" class="ui form small" autocomplete="off">
<div class="ui fields inline">
<div class="ui field">
<select class="ui dropdown" name="clusterId" v-model="clusterId">
@@ -13,14 +21,16 @@
<div class="ui field">
<select class="ui dropdown" name="nodeId" v-model="nodeId">
<option value="">[节点]</option>
<option v-for="node in nodes" :value="node.id" v-if="clusterId == '' || clusterId == node.clusterId">{{node.name}}</option>
<option v-for="node in nodes" :value="node.id" v-if="clusterId == '' || clusterId == node.clusterId">
{{node.name}}</option>
</select>
</div>
<div class="ui field">
<input type="text" name="dayFrom" placeholder="开始日期" v-model="dayFrom" style="width:8em" id="day-from-picker"/>
<input type="text" name="dayFrom" placeholder="开始日期" v-model="dayFrom" style="width:7.8em"
id="day-from-picker" />
</div>
<div class="ui field">
<input type="text" name="dayTo" placeholder="结束日期" v-model="dayTo" style="width:8em" id="day-to-picker"/>
<input type="text" name="dayTo" placeholder="结束日期" v-model="dayTo" style="width:7.8em" id="day-to-picker" />
</div>
<div class="ui field">
<select class="ui dropdown" name="level" v-model="level">
@@ -32,18 +42,21 @@
</select>
</div>
<div class="ui field">
<input type="text" name="keyword" style="width:15em" v-model="keyword" placeholder="类型/模块/详情/节点"/>
<input type="text" name="keyword" v-model="keyword" placeholder="类型/详情/节点" />
</div>
<div class="ui field">
<button type="submit" class="ui button">查询</button>
<button type="submit" class="ui button small">查询</button>
</div>
<div class="ui field" v-if="clusterId.toString().length > 0 || nodeId.toString().length > 0 || dayFrom.length > 0 || dayTo.length > 0 || keyword.length > 0 || level.length > 0">
<div class="ui field"
v-if="clusterId.toString().length > 0 || nodeId.toString().length > 0 || dayFrom.length > 0 || dayTo.length > 0 || keyword.length > 0 || level.length > 0">
<a href="/httpdns/runtimeLogs">[清除条件]</a>
</div>
</div>
</form>
<p class="comment" v-if="runtimeLogs.length == 0">暂时还没有运行日志。</p>
<div class="margin"></div>
<not-found-box v-if="runtimeLogs.length == 0">暂时还没有运行日志。</not-found-box>
<table class="ui table selectable celled" v-if="runtimeLogs.length > 0">
<thead>
@@ -53,10 +66,8 @@
<th>节点</th>
<th>级别</th>
<th>类型</th>
<th>模块</th>
<th>详情</th>
<th>次数</th>
<th>请求ID</th>
</tr>
</thead>
<tbody>
@@ -65,16 +76,13 @@
<td>{{log.clusterName}}</td>
<td>{{log.nodeName}}</td>
<td>
<span class="ui label tiny red" v-if="log.level == 'error'">error</span>
<span class="ui label tiny orange" v-else-if="log.level == 'warning'">warning</span>
<span class="ui label tiny blue" v-else-if="log.level == 'info'">info</span>
<span class="ui label tiny green" v-else>success</span>
<span
class="httpdns-runtime-level"
:class="{red:log.level == 'error', orange:log.level == 'warning', green:log.level == 'success'}">{{log.level}}</span>
</td>
<td><code>{{log.tag}}</code></td>
<td>{{log.module}}</td>
<td>{{log.description}}</td>
<td>{{log.count}}</td>
<td><code>{{log.requestId}}</code></td>
</tr>
</tbody>
</table>

View File

@@ -1,4 +1,4 @@
<!-- 左侧菜单由 Go Backend 自动生成注入,此处加首行子菜单使其符合标准平台样式 -->
<first-menu>
<menu-item href="/httpdns/sandbox" code="index">API 在线沙盒</menu-item>
<menu-item href="/httpdns/sandbox" code="index">解析测试</menu-item>
</first-menu>

View File

@@ -1,24 +1,6 @@
{$layout}
{$template "menu"}
<style>
.httpdns-sdns-row {
display: flex;
gap: .5em;
align-items: center;
margin-bottom: .45em;
}
.httpdns-sdns-row .field {
margin: 0 !important;
flex: 1;
}
.httpdns-sdns-actions {
margin-top: .2em;
}
.httpdns-sdns-actions .count {
color: #8f9aa6;
margin-left: .4em;
}
</style>
<div class="ui margin"></div>
<div>
<div class="ui grid stackable">
@@ -52,24 +34,6 @@
@click.prevent="request.qtype='AAAA'" type="button">AAAA</button>
</div>
</div>
<div class="field">
<label>SDNS 参数</label>
<div class="httpdns-sdns-row" v-for="(param, index) in sdnsParams">
<div class="field">
<input type="text" maxlength="64" placeholder="参数名称" v-model="param.name" />
</div>
<div class="field">
<input type="text" maxlength="64" placeholder="参数值" v-model="param.value" />
</div>
<a href="" @click.prevent="removeSDNSParam(index)" title="删除"><i class="icon trash alternate outline"></i></a>
</div>
<div class="httpdns-sdns-actions">
<a href="" @click.prevent="addSDNSParam" :class="{disabled: sdnsParams.length >= 10}">
<i class="icon plus circle"></i>添加参数
</a>
<span class="count">{{sdnsParams.length}}/10</span>
</div>
</div>
<div class="ui divider"></div>
@@ -123,7 +87,8 @@
<div class="column">
<div class="ui segment">
<div class="grey">客户端 IP</div>
<div style="margin-top:.4em;"><code>{{response.data.client_ip || request.clientIp || '-'}}</code></div>
<div style="margin-top:.4em;">
<code>{{response.data.client_ip || request.clientIp || '-'}}</code></div>
</div>
</div>
<div class="column">
@@ -155,7 +120,8 @@
</thead>
<tbody>
<tr v-for="(row, idx) in response.resultRows">
<td v-if="idx === 0" :rowspan="response.resultRows.length">{{row.domain || request.domain}}</td>
<td v-if="idx === 0" :rowspan="response.resultRows.length">{{row.domain ||
request.domain}}</td>
<td>{{row.type || request.qtype}}</td>
<td><code>{{row.ip}}</code></td>
<td>{{row.ttl}}s</td>

View File

@@ -9,7 +9,6 @@ Tea.context(function () {
}
this.request = this.newRequest()
this.sdnsParams = [{name: "", value: ""}]
this.response = {
hasResult: false,
@@ -26,39 +25,6 @@ Tea.context(function () {
this.apps = []
}
this.addSDNSParam = function () {
if (this.sdnsParams.length >= 10) {
return
}
this.sdnsParams.push({name: "", value: ""})
}
this.removeSDNSParam = function (index) {
if (index < 0 || index >= this.sdnsParams.length) {
return
}
this.sdnsParams.splice(index, 1)
if (this.sdnsParams.length == 0) {
this.sdnsParams.push({name: "", value: ""})
}
}
this.cleanSDNSParams = function () {
let list = []
this.sdnsParams.forEach(function (item) {
let name = (item.name || "").trim()
let value = (item.value || "").trim()
if (name.length == 0 && value.length == 0) {
return
}
list.push({
name: name,
value: value
})
})
return list
}
this.normalizeResultRows = function (data) {
if (typeof data == "undefined" || data == null) {
return []
@@ -106,7 +72,6 @@ Tea.context(function () {
this.response.hasResult = false
let payload = Object.assign({}, this.request)
payload.sdnsParamsJSON = JSON.stringify(this.cleanSDNSParams())
this.$post("/httpdns/sandbox/test")
.params(payload)
@@ -122,7 +87,6 @@ Tea.context(function () {
this.resetForm = function () {
this.request = this.newRequest()
this.sdnsParams = [{name: "", value: ""}]
this.response = {
hasResult: false,
code: -1,

View File

@@ -11,7 +11,7 @@
- 全局配置
- 应用管理
- SDK接入引导
- 解析日志
- 访问日志
- 运行日志
- 解析测试
2. 现阶段以管理端页面与 Mock 数据联调为主。
@@ -105,7 +105,7 @@
## 6. 日志页面(现状)
### 6.1 解析日志
### 6.1 访问日志
1. 主列:
- 集群
- 节点