feat: sync httpdns sdk/platform updates without large binaries
This commit is contained in:
@@ -1,4 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
function verify_components_bundle() {
|
||||
local file_path="$1"
|
||||
if [ ! -f "$file_path" ]; then
|
||||
echo "[error] components.js not found: $file_path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local file_size
|
||||
file_size=$(wc -c < "$file_path")
|
||||
if [ "$file_size" -lt 100000 ]; then
|
||||
echo "[error] components.js looks too small ($file_size bytes), generate likely failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! grep -q 'Vue.component("csrf-token"' "$file_path"; then
|
||||
echo "[error] components.js missing csrf-token component, generate likely failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "verify components.js: ok ($file_size bytes)"
|
||||
return 0
|
||||
}
|
||||
|
||||
function build() {
|
||||
ROOT=$(dirname "$0")
|
||||
@@ -58,7 +82,7 @@ function build() {
|
||||
|
||||
# generate files
|
||||
echo "generating files ..."
|
||||
env CGO_ENABLED=0 go run -tags $TAG "$ROOT"/../cmd/edge-admin/main.go generate
|
||||
env TEAROOT="$ROOT" CGO_ENABLED=0 go run -tags "$TAG" "$ROOT"/../cmd/edge-admin/main.go generate
|
||||
if [ "$(which uglifyjs)" ]; then
|
||||
echo "compress to component.js ..."
|
||||
uglifyjs --compress --mangle -- "${JS_ROOT}"/components.src.js > "${JS_ROOT}"/components.js
|
||||
@@ -69,6 +93,8 @@ function build() {
|
||||
cp "${JS_ROOT}"/utils.js "${JS_ROOT}"/utils.min.js
|
||||
fi
|
||||
|
||||
verify_components_bundle "${JS_ROOT}/components.js"
|
||||
|
||||
# create dir & copy files
|
||||
echo "copying ..."
|
||||
if [ ! -d "$DIST" ]; then
|
||||
|
||||
@@ -1,22 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
JS_ROOT=../web/public/js
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
JS_ROOT="$ROOT"/../web/public/js
|
||||
|
||||
function verify_components_bundle() {
|
||||
local file_path="$1"
|
||||
if [ ! -f "$file_path" ]; then
|
||||
echo "[error] components.js not found: $file_path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local file_size
|
||||
file_size=$(wc -c < "$file_path")
|
||||
if [ "$file_size" -lt 100000 ]; then
|
||||
echo "[error] components.js looks too small ($file_size bytes), generate likely failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! grep -q 'Vue.component("csrf-token"' "$file_path"; then
|
||||
echo "[error] components.js missing csrf-token component, generate likely failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "verify components.js: ok ($file_size bytes)"
|
||||
return 0
|
||||
}
|
||||
|
||||
echo "generating component.src.js ..."
|
||||
env CGO_ENABLED=0 go run -tags=community ../cmd/edge-admin/main.go generate
|
||||
env TEAROOT="$ROOT" CGO_ENABLED=0 go run -tags=community "$ROOT"/../cmd/edge-admin/main.go generate
|
||||
|
||||
if [ "$(which uglifyjs)" ]; then
|
||||
echo "compress to component.js ..."
|
||||
uglifyjs --compress --mangle -- ${JS_ROOT}/components.src.js > ${JS_ROOT}/components.js
|
||||
uglifyjs --compress --mangle -- "${JS_ROOT}"/components.src.js > "${JS_ROOT}"/components.js
|
||||
|
||||
echo "compress to utils.min.js ..."
|
||||
uglifyjs --compress --mangle -- ${JS_ROOT}/utils.js > ${JS_ROOT}/utils.min.js
|
||||
uglifyjs --compress --mangle -- "${JS_ROOT}"/utils.js > "${JS_ROOT}"/utils.min.js
|
||||
else
|
||||
echo "copy to component.js ..."
|
||||
cp ${JS_ROOT}/components.src.js ${JS_ROOT}/components.js
|
||||
cp "${JS_ROOT}"/components.src.js "${JS_ROOT}"/components.js
|
||||
|
||||
echo "copy to utils.min.js ..."
|
||||
cp ${JS_ROOT}/utils.js ${JS_ROOT}/utils.min.js
|
||||
cp "${JS_ROOT}"/utils.js "${JS_ROOT}"/utils.min.js
|
||||
fi
|
||||
|
||||
echo "ok"
|
||||
verify_components_bundle "${JS_ROOT}/components.js"
|
||||
|
||||
echo "ok"
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -112,10 +113,12 @@ func main() {
|
||||
}
|
||||
})
|
||||
app.On("generate", func() {
|
||||
prepareGenerateRoot()
|
||||
|
||||
err := gen.Generate()
|
||||
if err != nil {
|
||||
fmt.Println("generate failed: " + err.Error())
|
||||
return
|
||||
os.Exit(1)
|
||||
}
|
||||
})
|
||||
app.On("dev", func() {
|
||||
@@ -214,3 +217,32 @@ func main() {
|
||||
adminNode.Run()
|
||||
})
|
||||
}
|
||||
|
||||
func prepareGenerateRoot() {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
candidates := []string{
|
||||
wd,
|
||||
filepath.Clean(filepath.Join(wd, "..")),
|
||||
}
|
||||
|
||||
for _, root := range candidates {
|
||||
componentsDir := filepath.Join(root, "web", "public", "js", "components")
|
||||
stat, statErr := os.Stat(componentsDir)
|
||||
if statErr != nil || !stat.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// In testing mode, generator reads from Tea.Root + "/../web/...",
|
||||
// so keep Root under build dir to make relative path stable.
|
||||
buildRoot := filepath.Join(root, "build")
|
||||
Tea.UpdateRoot(buildRoot)
|
||||
Tea.SetPublicDir(filepath.Join(root, "web", "public"))
|
||||
Tea.SetViewsDir(filepath.Join(root, "web", "views"))
|
||||
Tea.SetTmpDir(filepath.Join(root, "web", "tmp"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
69
EdgeAdmin/internal/configloaders/upgrade_config.go
Normal file
69
EdgeAdmin/internal/configloaders/upgrade_config.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package configloaders
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
|
||||
)
|
||||
|
||||
const UpgradeSettingName = "upgradeConfig"
|
||||
|
||||
var sharedUpgradeConfig *systemconfigs.UpgradeConfig
|
||||
|
||||
func LoadUpgradeConfig() (*systemconfigs.UpgradeConfig, error) {
|
||||
locker.Lock()
|
||||
defer locker.Unlock()
|
||||
|
||||
if sharedUpgradeConfig != nil {
|
||||
return sharedUpgradeConfig, nil
|
||||
}
|
||||
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := rpcClient.SysSettingRPC().ReadSysSetting(rpcClient.Context(0), &pb.ReadSysSettingRequest{
|
||||
Code: UpgradeSettingName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resp.ValueJSON) == 0 {
|
||||
sharedUpgradeConfig = systemconfigs.NewUpgradeConfig()
|
||||
return sharedUpgradeConfig, nil
|
||||
}
|
||||
|
||||
config := systemconfigs.NewUpgradeConfig()
|
||||
err = json.Unmarshal(resp.ValueJSON, config)
|
||||
if err != nil {
|
||||
sharedUpgradeConfig = systemconfigs.NewUpgradeConfig()
|
||||
return sharedUpgradeConfig, nil
|
||||
}
|
||||
sharedUpgradeConfig = config
|
||||
return sharedUpgradeConfig, nil
|
||||
}
|
||||
|
||||
func UpdateUpgradeConfig(config *systemconfigs.UpgradeConfig) error {
|
||||
locker.Lock()
|
||||
defer locker.Unlock()
|
||||
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
valueJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = rpcClient.SysSettingRPC().UpdateSysSetting(rpcClient.Context(0), &pb.UpdateSysSettingRequest{
|
||||
Code: UpgradeSettingName,
|
||||
ValueJSON: valueJSON,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sharedUpgradeConfig = config
|
||||
return nil
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package teaconst
|
||||
|
||||
const (
|
||||
Version = "1.4.8" //1.3.9
|
||||
Version = "1.4.9" //1.3.9
|
||||
|
||||
APINodeVersion = "1.4.8" //1.3.9
|
||||
APINodeVersion = "1.4.9" //1.3.9
|
||||
|
||||
ProductName = "Edge Admin"
|
||||
ProcessName = "edge-admin"
|
||||
|
||||
@@ -28,19 +28,20 @@ func (this *SdkCheckAction) RunGet(params struct {
|
||||
return
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(params.Version)
|
||||
t := strings.ToLower(strings.TrimSpace(params.Type))
|
||||
if t == "doc" {
|
||||
docPath := findUploadedSDKDocPath(platform, params.Version)
|
||||
docPath := findUploadedSDKDocPath(platform, version)
|
||||
if len(docPath) == 0 {
|
||||
this.Data["exists"] = false
|
||||
this.Data["message"] = "Documentation is unavailable, please upload first"
|
||||
this.Data["message"] = "当前平台/版本尚未上传集成文档"
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
downloadURL := "/httpdns/apps/sdk/doc?platform=" + url.QueryEscape(platform)
|
||||
if len(strings.TrimSpace(params.Version)) > 0 {
|
||||
downloadURL += "&version=" + url.QueryEscape(strings.TrimSpace(params.Version))
|
||||
if len(version) > 0 {
|
||||
downloadURL += "&version=" + url.QueryEscape(version)
|
||||
}
|
||||
this.Data["exists"] = true
|
||||
this.Data["url"] = downloadURL
|
||||
@@ -48,17 +49,17 @@ func (this *SdkCheckAction) RunGet(params struct {
|
||||
return
|
||||
}
|
||||
|
||||
archivePath := findSDKArchivePath(filename, params.Version)
|
||||
archivePath := findSDKArchivePath(filename, version)
|
||||
if len(archivePath) == 0 {
|
||||
this.Data["exists"] = false
|
||||
this.Data["message"] = "SDK package is unavailable, please upload first"
|
||||
this.Data["message"] = "当前平台/版本尚未上传 SDK 安装包"
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
downloadURL := "/httpdns/apps/sdk/download?platform=" + url.QueryEscape(platform)
|
||||
if len(strings.TrimSpace(params.Version)) > 0 {
|
||||
downloadURL += "&version=" + url.QueryEscape(strings.TrimSpace(params.Version))
|
||||
if len(version) > 0 {
|
||||
downloadURL += "&version=" + url.QueryEscape(version)
|
||||
}
|
||||
downloadURL += "&raw=1"
|
||||
this.Data["exists"] = true
|
||||
|
||||
@@ -3,7 +3,6 @@ package apps
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
)
|
||||
@@ -20,7 +19,7 @@ func (this *SdkDocAction) RunGet(params struct {
|
||||
Platform string
|
||||
Version string
|
||||
}) {
|
||||
platform, _, readmeRelativePath, _, err := resolveSDKPlatform(params.Platform)
|
||||
platform, _, _, _, err := resolveSDKPlatform(params.Platform)
|
||||
if err != nil {
|
||||
this.Data["exists"] = false
|
||||
this.Data["message"] = err.Error()
|
||||
@@ -28,35 +27,25 @@ func (this *SdkDocAction) RunGet(params struct {
|
||||
return
|
||||
}
|
||||
|
||||
var data []byte
|
||||
uploadedDocPath := findUploadedSDKDocPath(platform, params.Version)
|
||||
if len(uploadedDocPath) > 0 {
|
||||
data, err = os.ReadFile(uploadedDocPath)
|
||||
}
|
||||
|
||||
sdkRoot, sdkRootErr := findSDKRoot()
|
||||
if len(data) == 0 && sdkRootErr == nil {
|
||||
readmePath := filepath.Join(sdkRoot, readmeRelativePath)
|
||||
data, err = os.ReadFile(readmePath)
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
localDocPath := findLocalSDKDocPath(platform)
|
||||
if len(localDocPath) > 0 {
|
||||
data, err = os.ReadFile(localDocPath)
|
||||
}
|
||||
}
|
||||
|
||||
if len(data) == 0 || err != nil {
|
||||
docPath := findUploadedSDKDocPath(platform, params.Version)
|
||||
if len(docPath) == 0 {
|
||||
this.Data["exists"] = false
|
||||
this.Data["message"] = "SDK documentation is not found on server, please upload first"
|
||||
this.Data["message"] = "当前平台/版本尚未上传集成文档"
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
downloadName := filepath.Base(uploadedDocPath)
|
||||
data, err := os.ReadFile(docPath)
|
||||
if err != nil || len(data) == 0 {
|
||||
this.Data["exists"] = false
|
||||
this.Data["message"] = "读取集成文档失败"
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
downloadName := filepath.Base(docPath)
|
||||
if len(downloadName) == 0 || downloadName == "." || downloadName == string(filepath.Separator) {
|
||||
downloadName = "httpdns-sdk-" + strings.ToLower(platform) + ".md"
|
||||
downloadName = "sdk-doc.md"
|
||||
}
|
||||
|
||||
this.AddHeader("Content-Type", "text/markdown; charset=utf-8")
|
||||
|
||||
@@ -32,7 +32,7 @@ func (this *SdkDownloadAction) RunGet(params struct {
|
||||
archivePath := findSDKArchivePath(filename, params.Version)
|
||||
if len(archivePath) == 0 {
|
||||
this.Data["exists"] = false
|
||||
this.Data["message"] = "SDK archive not found on server, please upload first: " + filename
|
||||
this.Data["message"] = "当前平台/版本尚未上传 SDK 安装包"
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
@@ -40,7 +40,7 @@ func (this *SdkDownloadAction) RunGet(params struct {
|
||||
fp, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
this.Data["exists"] = false
|
||||
this.Data["message"] = "failed to open SDK archive: " + err.Error()
|
||||
this.Data["message"] = "打开 SDK 安装包失败: " + err.Error()
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,16 +1,183 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
)
|
||||
|
||||
type sdkUploadMeta struct {
|
||||
Platform string `json:"platform"`
|
||||
Version string `json:"version"`
|
||||
FileType string `json:"fileType"` // sdk | doc
|
||||
Filename string `json:"filename"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type sdkUploadMetaRecord struct {
|
||||
Meta sdkUploadMeta
|
||||
Dir string
|
||||
FilePath string
|
||||
}
|
||||
|
||||
func sdkUploadMetaFilename(platform string, version string, fileType string) string {
|
||||
platform = strings.ToLower(strings.TrimSpace(platform))
|
||||
version = strings.TrimSpace(version)
|
||||
fileType = strings.ToLower(strings.TrimSpace(fileType))
|
||||
return ".httpdns-sdk-meta-" + platform + "-v" + version + "-" + fileType + ".json"
|
||||
}
|
||||
|
||||
func isSDKUploadMetaFile(name string) bool {
|
||||
name = strings.ToLower(strings.TrimSpace(name))
|
||||
return strings.HasPrefix(name, ".httpdns-sdk-meta-") && strings.HasSuffix(name, ".json")
|
||||
}
|
||||
|
||||
func parseSDKPlatformFromDownloadFilename(downloadFilename string) string {
|
||||
name := strings.ToLower(strings.TrimSpace(downloadFilename))
|
||||
if !strings.HasPrefix(name, "httpdns-sdk-") || !strings.HasSuffix(name, ".zip") {
|
||||
return ""
|
||||
}
|
||||
|
||||
platform := strings.TrimSuffix(strings.TrimPrefix(name, "httpdns-sdk-"), ".zip")
|
||||
switch platform {
|
||||
case "android", "ios", "flutter":
|
||||
return platform
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func listSDKUploadMetaRecords() []sdkUploadMetaRecord {
|
||||
type wrapped struct {
|
||||
record sdkUploadMetaRecord
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
byKey := map[string]wrapped{}
|
||||
for _, dir := range sdkUploadSearchDirs() {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := entry.Name()
|
||||
if !isSDKUploadMetaFile(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
metaPath := filepath.Join(dir, name)
|
||||
data, err := os.ReadFile(metaPath)
|
||||
if err != nil || len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var meta sdkUploadMeta
|
||||
if err = json.Unmarshal(data, &meta); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
meta.Platform = strings.ToLower(strings.TrimSpace(meta.Platform))
|
||||
meta.Version = strings.TrimSpace(meta.Version)
|
||||
meta.FileType = strings.ToLower(strings.TrimSpace(meta.FileType))
|
||||
meta.Filename = filepath.Base(strings.TrimSpace(meta.Filename))
|
||||
if len(meta.Platform) == 0 || len(meta.Version) == 0 || len(meta.Filename) == 0 {
|
||||
continue
|
||||
}
|
||||
if meta.FileType != "sdk" && meta.FileType != "doc" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(meta.Filename, "..") || strings.Contains(meta.Filename, "/") || strings.Contains(meta.Filename, "\\") {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(dir, meta.Filename)
|
||||
fileStat, err := os.Stat(filePath)
|
||||
if err != nil || fileStat.IsDir() || fileStat.Size() <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
metaStat, err := os.Stat(metaPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if meta.UpdatedAt <= 0 {
|
||||
meta.UpdatedAt = metaStat.ModTime().Unix()
|
||||
}
|
||||
|
||||
key := meta.Platform + "|" + meta.Version + "|" + meta.FileType
|
||||
current := wrapped{
|
||||
record: sdkUploadMetaRecord{
|
||||
Meta: meta,
|
||||
Dir: dir,
|
||||
FilePath: filePath,
|
||||
},
|
||||
modTime: metaStat.ModTime(),
|
||||
}
|
||||
old, ok := byKey[key]
|
||||
if !ok ||
|
||||
current.record.Meta.UpdatedAt > old.record.Meta.UpdatedAt ||
|
||||
(current.record.Meta.UpdatedAt == old.record.Meta.UpdatedAt && current.modTime.After(old.modTime)) ||
|
||||
(current.record.Meta.UpdatedAt == old.record.Meta.UpdatedAt && current.modTime.Equal(old.modTime) && current.record.FilePath > old.record.FilePath) {
|
||||
byKey[key] = current
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]sdkUploadMetaRecord, 0, len(byKey))
|
||||
for _, item := range byKey {
|
||||
result = append(result, item.record)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func findSDKUploadFileByMeta(platform string, version string, fileType string) string {
|
||||
platform = strings.ToLower(strings.TrimSpace(platform))
|
||||
version = strings.TrimSpace(version)
|
||||
fileType = strings.ToLower(strings.TrimSpace(fileType))
|
||||
if len(platform) == 0 || len(version) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, record := range listSDKUploadMetaRecords() {
|
||||
if record.Meta.Platform == platform && record.Meta.Version == version && record.Meta.FileType == fileType {
|
||||
return record.FilePath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findNewestSDKUploadFileByMeta(platform string, fileType string) string {
|
||||
platform = strings.ToLower(strings.TrimSpace(platform))
|
||||
fileType = strings.ToLower(strings.TrimSpace(fileType))
|
||||
if len(platform) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var foundPath string
|
||||
var foundUpdatedAt int64
|
||||
for _, record := range listSDKUploadMetaRecords() {
|
||||
if record.Meta.Platform != platform || record.Meta.FileType != fileType {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(foundPath) == 0 || record.Meta.UpdatedAt > foundUpdatedAt || (record.Meta.UpdatedAt == foundUpdatedAt && record.FilePath > foundPath) {
|
||||
foundPath = record.FilePath
|
||||
foundUpdatedAt = record.Meta.UpdatedAt
|
||||
}
|
||||
}
|
||||
return foundPath
|
||||
}
|
||||
|
||||
func sdkUploadDir() string {
|
||||
dirs := sdkUploadDirs()
|
||||
if len(dirs) > 0 {
|
||||
@@ -27,6 +194,7 @@ func sdkUploadDirs() []string {
|
||||
filepath.Clean(Tea.Root + "/../edge-user/data/httpdns/sdk"),
|
||||
filepath.Clean(Tea.Root + "/../../data/httpdns/sdk"),
|
||||
}
|
||||
|
||||
results := make([]string, 0, len(candidates))
|
||||
seen := map[string]bool{}
|
||||
for _, dir := range candidates {
|
||||
@@ -54,67 +222,6 @@ func sdkUploadSearchDirs() []string {
|
||||
return results
|
||||
}
|
||||
|
||||
func findFirstExistingDir(paths []string) string {
|
||||
for _, path := range paths {
|
||||
stat, err := os.Stat(path)
|
||||
if err == nil && stat.IsDir() {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findFirstExistingFile(paths []string) string {
|
||||
for _, path := range paths {
|
||||
stat, err := os.Stat(path)
|
||||
if err == nil && !stat.IsDir() && stat.Size() > 0 {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findNewestExistingFile(paths []string) string {
|
||||
type fileInfo struct {
|
||||
path string
|
||||
modTime time.Time
|
||||
}
|
||||
result := fileInfo{}
|
||||
for _, path := range paths {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil || stat.IsDir() {
|
||||
continue
|
||||
}
|
||||
if stat.Size() <= 0 {
|
||||
continue
|
||||
}
|
||||
if len(result.path) == 0 || stat.ModTime().After(result.modTime) || (stat.ModTime().Equal(result.modTime) && path > result.path) {
|
||||
result.path = path
|
||||
result.modTime = stat.ModTime()
|
||||
}
|
||||
}
|
||||
return result.path
|
||||
}
|
||||
|
||||
func findSDKRoot() (string, error) {
|
||||
candidates := []string{
|
||||
filepath.Clean(Tea.Root + "/EdgeHttpDNS/sdk"),
|
||||
filepath.Clean(Tea.Root + "/edge-httpdns/sdk"),
|
||||
filepath.Clean(Tea.Root + "/edge-httpdns/edge-httpdns/sdk"),
|
||||
filepath.Clean(Tea.Root + "/../EdgeHttpDNS/sdk"),
|
||||
filepath.Clean(Tea.Root + "/../../EdgeHttpDNS/sdk"),
|
||||
filepath.Clean(Tea.Root + "/../edge-httpdns/sdk"),
|
||||
filepath.Clean(Tea.Root + "/../../edge-httpdns/sdk"),
|
||||
}
|
||||
|
||||
dir := findFirstExistingDir(candidates)
|
||||
if len(dir) > 0 {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
return "", errors.New("SDK files are not found on current server")
|
||||
}
|
||||
|
||||
func resolveSDKPlatform(platform string) (key string, relativeDir string, readmeRelativePath string, downloadFilename string, err error) {
|
||||
switch strings.ToLower(strings.TrimSpace(platform)) {
|
||||
case "android":
|
||||
@@ -122,52 +229,23 @@ func resolveSDKPlatform(platform string) (key string, relativeDir string, readme
|
||||
case "ios":
|
||||
return "ios", "ios", "ios/README.md", "httpdns-sdk-ios.zip", nil
|
||||
case "flutter":
|
||||
return "flutter", "flutter/aliyun_httpdns", "flutter/aliyun_httpdns/README.md", "httpdns-sdk-flutter.zip", nil
|
||||
return "flutter", "flutter/new_httpdns", "flutter/new_httpdns/README.md", "httpdns-sdk-flutter.zip", nil
|
||||
default:
|
||||
return "", "", "", "", errors.New("invalid platform, expected one of: android, ios, flutter")
|
||||
return "", "", "", "", errors.New("不支持的平台,可选值:android、ios、flutter")
|
||||
}
|
||||
}
|
||||
|
||||
func findSDKArchivePath(downloadFilename string, version string) string {
|
||||
searchDirs := sdkUploadSearchDirs()
|
||||
|
||||
normalizedVersion := strings.TrimSpace(version)
|
||||
base := strings.TrimSuffix(downloadFilename, ".zip")
|
||||
if len(normalizedVersion) > 0 {
|
||||
versionFiles := []string{}
|
||||
for _, dir := range searchDirs {
|
||||
versionFiles = append(versionFiles, filepath.Join(dir, base+"-v"+normalizedVersion+".zip"))
|
||||
}
|
||||
if path := findFirstExistingFile(versionFiles); len(path) > 0 {
|
||||
return path
|
||||
}
|
||||
platform := parseSDKPlatformFromDownloadFilename(downloadFilename)
|
||||
if len(platform) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
patternName := base + "-v*.zip"
|
||||
matches := []string{}
|
||||
for _, dir := range searchDirs {
|
||||
found, _ := filepath.Glob(filepath.Join(dir, patternName))
|
||||
for _, file := range found {
|
||||
stat, err := os.Stat(file)
|
||||
if err == nil && !stat.IsDir() {
|
||||
matches = append(matches, file)
|
||||
}
|
||||
}
|
||||
normalizedVersion := strings.TrimSpace(version)
|
||||
if len(normalizedVersion) > 0 {
|
||||
return findSDKUploadFileByMeta(platform, normalizedVersion, "sdk")
|
||||
}
|
||||
if len(matches) > 0 {
|
||||
return findNewestExistingFile(matches)
|
||||
}
|
||||
|
||||
exactFiles := []string{}
|
||||
for _, dir := range searchDirs {
|
||||
exactFiles = append(exactFiles, filepath.Join(dir, downloadFilename))
|
||||
}
|
||||
if path := findFirstExistingFile(exactFiles); len(path) > 0 {
|
||||
return path
|
||||
}
|
||||
|
||||
return ""
|
||||
return findNewestSDKUploadFileByMeta(platform, "sdk")
|
||||
}
|
||||
|
||||
func findUploadedSDKDocPath(platform string, version string) string {
|
||||
@@ -176,42 +254,9 @@ func findUploadedSDKDocPath(platform string, version string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
searchDirs := sdkUploadSearchDirs()
|
||||
normalizedVersion := strings.TrimSpace(version)
|
||||
if len(normalizedVersion) > 0 {
|
||||
exactVersion := []string{}
|
||||
for _, dir := range searchDirs {
|
||||
exactVersion = append(exactVersion, filepath.Join(dir, "httpdns-sdk-"+platform+"-v"+normalizedVersion+".md"))
|
||||
}
|
||||
if file := findFirstExistingFile(exactVersion); len(file) > 0 {
|
||||
return file
|
||||
}
|
||||
return ""
|
||||
return findSDKUploadFileByMeta(platform, normalizedVersion, "doc")
|
||||
}
|
||||
|
||||
matches := []string{}
|
||||
for _, dir := range searchDirs {
|
||||
pattern := filepath.Join(dir, "httpdns-sdk-"+platform+"-v*.md")
|
||||
found, _ := filepath.Glob(pattern)
|
||||
matches = append(matches, found...)
|
||||
}
|
||||
if len(matches) > 0 {
|
||||
sort.Strings(matches)
|
||||
return findNewestExistingFile(matches)
|
||||
}
|
||||
|
||||
exact := []string{}
|
||||
for _, dir := range searchDirs {
|
||||
exact = append(exact, filepath.Join(dir, "httpdns-sdk-"+platform+".md"))
|
||||
}
|
||||
return findFirstExistingFile(exact)
|
||||
}
|
||||
|
||||
func findLocalSDKDocPath(platform string) string {
|
||||
filename := strings.ToLower(strings.TrimSpace(platform)) + ".md"
|
||||
candidates := []string{
|
||||
filepath.Clean(Tea.Root + "/edge-admin/web/views/@default/httpdns/apps/docs/" + filename),
|
||||
filepath.Clean(Tea.Root + "/EdgeAdmin/web/views/@default/httpdns/apps/docs/" + filename),
|
||||
}
|
||||
return findFirstExistingFile(candidates)
|
||||
return findNewestSDKUploadFileByMeta(platform, "doc")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -14,6 +15,8 @@ import (
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
)
|
||||
|
||||
const sdkUploadMaxFileSize = 20 * 1024 * 1024 // 20MB
|
||||
|
||||
type SdkUploadAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
@@ -52,7 +55,7 @@ func (this *SdkUploadAction) RunPost(params struct {
|
||||
}) {
|
||||
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
|
||||
|
||||
platform, _, _, downloadFilename, err := resolveSDKPlatform(params.Platform)
|
||||
platform, _, _, _, err := resolveSDKPlatform(params.Platform)
|
||||
if err != nil {
|
||||
this.Fail(err.Error())
|
||||
return
|
||||
@@ -70,57 +73,21 @@ func (this *SdkUploadAction) RunPost(params struct {
|
||||
}
|
||||
|
||||
uploadDir := sdkUploadDir()
|
||||
err = os.MkdirAll(uploadDir, 0755)
|
||||
if err != nil {
|
||||
if err = os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
this.Fail("创建上传目录失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if params.SdkFile != nil {
|
||||
filename := strings.ToLower(strings.TrimSpace(params.SdkFile.Filename))
|
||||
if !strings.HasSuffix(filename, ".zip") {
|
||||
this.Fail("SDK 包仅支持 .zip 文件")
|
||||
return
|
||||
}
|
||||
|
||||
sdkData, readErr := params.SdkFile.Read()
|
||||
if readErr != nil {
|
||||
this.Fail("读取 SDK 包失败: " + readErr.Error())
|
||||
return
|
||||
}
|
||||
if len(sdkData) == 0 {
|
||||
this.Fail("SDK 包文件为空,请重新上传")
|
||||
return
|
||||
}
|
||||
|
||||
baseName := strings.TrimSuffix(downloadFilename, ".zip")
|
||||
err = saveSDKUploadFile(uploadDir, baseName+"-v"+version+".zip", sdkData)
|
||||
if err != nil {
|
||||
this.Fail("保存 SDK 包失败: " + err.Error())
|
||||
if err = this.saveUploadedItem(uploadDir, platform, version, "sdk", params.SdkFile); err != nil {
|
||||
this.Fail(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if params.DocFile != nil {
|
||||
docName := strings.ToLower(strings.TrimSpace(params.DocFile.Filename))
|
||||
if !strings.HasSuffix(docName, ".md") {
|
||||
this.Fail("集成文档仅支持 .md 文件")
|
||||
return
|
||||
}
|
||||
|
||||
docData, readErr := params.DocFile.Read()
|
||||
if readErr != nil {
|
||||
this.Fail("读取集成文档失败: " + readErr.Error())
|
||||
return
|
||||
}
|
||||
if len(docData) == 0 {
|
||||
this.Fail("集成文档文件为空,请重新上传")
|
||||
return
|
||||
}
|
||||
|
||||
err = saveSDKUploadFile(uploadDir, "httpdns-sdk-"+platform+"-v"+version+".md", docData)
|
||||
if err != nil {
|
||||
this.Fail("保存集成文档失败: " + err.Error())
|
||||
if err = this.saveUploadedItem(uploadDir, platform, version, "doc", params.DocFile); err != nil {
|
||||
this.Fail(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -128,6 +95,52 @@ func (this *SdkUploadAction) RunPost(params struct {
|
||||
this.Success()
|
||||
}
|
||||
|
||||
func (this *SdkUploadAction) saveUploadedItem(uploadDir string, platform string, version string, fileType string, file *actions.File) error {
|
||||
expectedExt := ".md"
|
||||
displayType := "集成文档"
|
||||
if fileType == "sdk" {
|
||||
expectedExt = ".zip"
|
||||
displayType = "SDK 包"
|
||||
}
|
||||
|
||||
filename, err := normalizeUploadedFilename(file.Filename, expectedExt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if file.Size > sdkUploadMaxFileSize {
|
||||
return errors.New(displayType + "文件不能超过 20MB")
|
||||
}
|
||||
|
||||
data, err := file.Read()
|
||||
if err != nil {
|
||||
return errors.New("读取" + displayType + "失败: " + err.Error())
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return errors.New(displayType + "文件为空,请重新上传")
|
||||
}
|
||||
if len(data) > sdkUploadMaxFileSize {
|
||||
return errors.New(displayType + "文件不能超过 20MB")
|
||||
}
|
||||
|
||||
if err = saveSDKUploadFile(uploadDir, filename, data); err != nil {
|
||||
return errors.New("保存" + displayType + "失败: " + err.Error())
|
||||
}
|
||||
|
||||
err = saveSDKUploadMetaRecord(uploadDir, sdkUploadMeta{
|
||||
Platform: platform,
|
||||
Version: version,
|
||||
FileType: fileType,
|
||||
Filename: filename,
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.New("保存上传元信息失败: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeSDKVersion(version string) (string, error) {
|
||||
version = strings.TrimSpace(version)
|
||||
if len(version) == 0 {
|
||||
@@ -142,6 +155,26 @@ func normalizeSDKVersion(version string) (string, error) {
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func normalizeUploadedFilename(raw string, expectedExt string) (string, error) {
|
||||
filename := filepath.Base(strings.TrimSpace(raw))
|
||||
if len(filename) == 0 || filename == "." || filename == string(filepath.Separator) {
|
||||
return "", errors.New("文件名不能为空")
|
||||
}
|
||||
if strings.Contains(filename, "/") || strings.Contains(filename, "\\") || strings.Contains(filename, "..") {
|
||||
return "", errors.New("文件名不合法")
|
||||
}
|
||||
|
||||
actualExt := strings.ToLower(filepath.Ext(filename))
|
||||
if actualExt != strings.ToLower(expectedExt) {
|
||||
if expectedExt == ".zip" {
|
||||
return "", errors.New("SDK 包仅支持 .zip 文件")
|
||||
}
|
||||
return "", errors.New("集成文档仅支持 .md 文件")
|
||||
}
|
||||
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
func saveSDKUploadFile(baseDir string, filename string, data []byte) error {
|
||||
targetPath := filepath.Join(baseDir, filename)
|
||||
tmpPath := targetPath + ".tmp"
|
||||
@@ -152,6 +185,35 @@ func saveSDKUploadFile(baseDir string, filename string, data []byte) error {
|
||||
return os.Rename(tmpPath, targetPath)
|
||||
}
|
||||
|
||||
func saveSDKUploadMetaRecord(baseDir string, meta sdkUploadMeta) error {
|
||||
meta.Platform = strings.ToLower(strings.TrimSpace(meta.Platform))
|
||||
meta.Version = strings.TrimSpace(meta.Version)
|
||||
meta.FileType = strings.ToLower(strings.TrimSpace(meta.FileType))
|
||||
meta.Filename = filepath.Base(strings.TrimSpace(meta.Filename))
|
||||
if len(meta.Platform) == 0 || len(meta.Version) == 0 || len(meta.FileType) == 0 || len(meta.Filename) == 0 {
|
||||
return errors.New("上传元信息不完整")
|
||||
}
|
||||
|
||||
metaFilename := sdkUploadMetaFilename(meta.Platform, meta.Version, meta.FileType)
|
||||
metaPath := filepath.Join(baseDir, metaFilename)
|
||||
if data, err := os.ReadFile(metaPath); err == nil && len(data) > 0 {
|
||||
var oldMeta sdkUploadMeta
|
||||
if json.Unmarshal(data, &oldMeta) == nil {
|
||||
oldFile := filepath.Base(strings.TrimSpace(oldMeta.Filename))
|
||||
if len(oldFile) > 0 && oldFile != meta.Filename {
|
||||
_ = os.Remove(filepath.Join(baseDir, oldFile))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return saveSDKUploadFile(baseDir, metaFilename, data)
|
||||
}
|
||||
|
||||
func listUploadedSDKFiles() []map[string]interface{} {
|
||||
type item struct {
|
||||
Name string
|
||||
@@ -162,45 +224,26 @@ func listUploadedSDKFiles() []map[string]interface{} {
|
||||
UpdatedAt int64
|
||||
}
|
||||
|
||||
byName := map[string]item{}
|
||||
for _, dir := range sdkUploadSearchDirs() {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
items := make([]item, 0)
|
||||
for _, record := range listSDKUploadMetaRecords() {
|
||||
stat, err := os.Stat(record.FilePath)
|
||||
if err != nil || stat.IsDir() {
|
||||
continue
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
platform, version, fileType, ok := parseSDKUploadFilename(name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
info, statErr := entry.Info()
|
||||
if statErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
current := item{
|
||||
Name: name,
|
||||
Platform: platform,
|
||||
FileType: fileType,
|
||||
Version: version,
|
||||
SizeBytes: info.Size(),
|
||||
UpdatedAt: info.ModTime().Unix(),
|
||||
}
|
||||
old, exists := byName[name]
|
||||
if !exists || current.UpdatedAt >= old.UpdatedAt {
|
||||
byName[name] = current
|
||||
}
|
||||
fileType := "SDK 包"
|
||||
if record.Meta.FileType == "doc" {
|
||||
fileType = "集成文档"
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]item, 0, len(byName))
|
||||
for _, it := range byName {
|
||||
items = append(items, it)
|
||||
items = append(items, item{
|
||||
Name: filepath.Base(record.FilePath),
|
||||
Platform: record.Meta.Platform,
|
||||
FileType: fileType,
|
||||
Version: record.Meta.Version,
|
||||
SizeBytes: stat.Size(),
|
||||
UpdatedAt: stat.ModTime().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
@@ -224,55 +267,21 @@ func listUploadedSDKFiles() []map[string]interface{} {
|
||||
return result
|
||||
}
|
||||
|
||||
func parseSDKUploadFilename(filename string) (platform string, version string, fileType string, ok bool) {
|
||||
if !strings.HasPrefix(filename, "httpdns-sdk-") {
|
||||
return "", "", "", false
|
||||
}
|
||||
|
||||
ext := ""
|
||||
switch {
|
||||
case strings.HasSuffix(filename, ".zip"):
|
||||
ext = ".zip"
|
||||
fileType = "SDK包"
|
||||
case strings.HasSuffix(filename, ".md"):
|
||||
ext = ".md"
|
||||
fileType = "集成文档"
|
||||
default:
|
||||
return "", "", "", false
|
||||
}
|
||||
|
||||
main := strings.TrimSuffix(strings.TrimPrefix(filename, "httpdns-sdk-"), ext)
|
||||
version = ""
|
||||
if idx := strings.Index(main, "-v"); idx > 0 && idx+2 < len(main) {
|
||||
version = main[idx+2:]
|
||||
main = main[:idx]
|
||||
}
|
||||
|
||||
main = strings.ToLower(strings.TrimSpace(main))
|
||||
switch main {
|
||||
case "android", "ios", "flutter":
|
||||
platform = main
|
||||
if len(version) == 0 {
|
||||
version = "-"
|
||||
}
|
||||
return platform, version, fileType, true
|
||||
default:
|
||||
return "", "", "", false
|
||||
}
|
||||
}
|
||||
|
||||
func formatSDKFileSize(size int64) string {
|
||||
if size < 1024 {
|
||||
return strconv.FormatInt(size, 10) + " B"
|
||||
}
|
||||
|
||||
sizeKB := float64(size) / 1024
|
||||
if sizeKB < 1024 {
|
||||
return strconv.FormatFloat(sizeKB, 'f', 1, 64) + " KB"
|
||||
}
|
||||
|
||||
sizeMB := sizeKB / 1024
|
||||
if sizeMB < 1024 {
|
||||
return strconv.FormatFloat(sizeMB, 'f', 1, 64) + " MB"
|
||||
}
|
||||
|
||||
sizeGB := sizeMB / 1024
|
||||
return strconv.FormatFloat(sizeGB, 'f', 1, 64) + " GB"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -34,19 +35,16 @@ func (this *SdkUploadDeleteAction) RunPost(params struct {
|
||||
this.Fail("文件名不合法")
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(filename, "httpdns-sdk-") {
|
||||
this.Fail("不允许删除该文件")
|
||||
return
|
||||
}
|
||||
if !(strings.HasSuffix(filename, ".zip") || strings.HasSuffix(filename, ".md")) {
|
||||
this.Fail("不允许删除该文件")
|
||||
lowName := strings.ToLower(filename)
|
||||
if !strings.HasSuffix(lowName, ".zip") && !strings.HasSuffix(lowName, ".md") {
|
||||
this.Fail("仅允许删除 .zip 或 .md 文件")
|
||||
return
|
||||
}
|
||||
|
||||
for _, dir := range sdkUploadDirs() {
|
||||
fullPath := filepath.Join(dir, filename)
|
||||
_, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
stat, err := os.Stat(fullPath)
|
||||
if err != nil || stat.IsDir() {
|
||||
continue
|
||||
}
|
||||
if err = os.Remove(fullPath); err != nil {
|
||||
@@ -55,5 +53,38 @@ func (this *SdkUploadDeleteAction) RunPost(params struct {
|
||||
}
|
||||
}
|
||||
|
||||
// 删除引用该文件的元数据
|
||||
for _, dir := range sdkUploadDirs() {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
if !isSDKUploadMetaFile(entry.Name()) {
|
||||
continue
|
||||
}
|
||||
|
||||
metaPath := filepath.Join(dir, entry.Name())
|
||||
data, err := os.ReadFile(metaPath)
|
||||
if err != nil || len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var meta sdkUploadMeta
|
||||
if err = json.Unmarshal(data, &meta); err != nil {
|
||||
continue
|
||||
}
|
||||
if filepath.Base(strings.TrimSpace(meta.Filename)) != filename {
|
||||
continue
|
||||
}
|
||||
|
||||
_ = os.Remove(metaPath)
|
||||
}
|
||||
}
|
||||
|
||||
this.Success()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||
)
|
||||
|
||||
@@ -50,7 +52,22 @@ func (this *IndexAction) RunGet(params struct {
|
||||
this.Data["nodeDatetime"] = nodeDatetime
|
||||
this.Data["nodeTimeDiff"] = nodeTimeDiff
|
||||
|
||||
this.Data["shouldUpgrade"] = false
|
||||
this.Data["newVersion"] = ""
|
||||
osName := strings.TrimSpace(status.GetString("os"))
|
||||
if len(osName) > 0 {
|
||||
checkVersionResp, err := this.RPC().HTTPDNSNodeRPC().CheckHTTPDNSNodeLatestVersion(this.AdminContext(), &pb.CheckHTTPDNSNodeLatestVersionRequest{
|
||||
Os: osName,
|
||||
Arch: strings.TrimSpace(status.GetString("arch")),
|
||||
CurrentVersion: strings.TrimSpace(status.GetString("buildVersion")),
|
||||
})
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
this.Data["shouldUpgrade"] = checkVersionResp.GetHasNewVersion()
|
||||
this.Data["newVersion"] = checkVersionResp.GetNewVersion()
|
||||
} else {
|
||||
this.Data["shouldUpgrade"] = false
|
||||
this.Data["newVersion"] = ""
|
||||
}
|
||||
this.Show()
|
||||
}
|
||||
|
||||
@@ -22,14 +22,14 @@ func findHTTPDNSClusterMap(parent *actionutils.ParentAction, clusterID int64) (m
|
||||
return maps.Map{
|
||||
"id": clusterID,
|
||||
"name": "",
|
||||
"installDir": "/opt/edge-httpdns",
|
||||
"installDir": "/root/edge-httpdns",
|
||||
}, nil
|
||||
}
|
||||
|
||||
cluster := resp.GetCluster()
|
||||
installDir := strings.TrimSpace(cluster.GetInstallDir())
|
||||
if len(installDir) == 0 {
|
||||
installDir = "/opt/edge-httpdns"
|
||||
installDir = "/root/edge-httpdns"
|
||||
}
|
||||
return maps.Map{
|
||||
"id": cluster.GetId(),
|
||||
@@ -93,7 +93,7 @@ func findHTTPDNSNodeMap(parent *actionutils.ParentAction, nodeID int64) (maps.Ma
|
||||
|
||||
installDir := strings.TrimSpace(node.GetInstallDir())
|
||||
if len(installDir) == 0 {
|
||||
installDir = "/opt/edge-httpdns"
|
||||
installDir = "/root/edge-httpdns"
|
||||
}
|
||||
|
||||
clusterMap, err := findHTTPDNSClusterMap(parent, node.GetClusterId())
|
||||
@@ -137,22 +137,22 @@ func findHTTPDNSNodeMap(parent *actionutils.ParentAction, nodeID int64) (maps.Ma
|
||||
}
|
||||
|
||||
return maps.Map{
|
||||
"id": node.GetId(),
|
||||
"clusterId": node.GetClusterId(),
|
||||
"name": node.GetName(),
|
||||
"isOn": node.GetIsOn(),
|
||||
"isUp": node.GetIsUp(),
|
||||
"isInstalled": node.GetIsInstalled(),
|
||||
"isActive": node.GetIsActive(),
|
||||
"uniqueId": node.GetUniqueId(),
|
||||
"secret": node.GetSecret(),
|
||||
"installDir": installDir,
|
||||
"status": statusMap,
|
||||
"id": node.GetId(),
|
||||
"clusterId": node.GetClusterId(),
|
||||
"name": node.GetName(),
|
||||
"isOn": node.GetIsOn(),
|
||||
"isUp": node.GetIsUp(),
|
||||
"isInstalled": node.GetIsInstalled(),
|
||||
"isActive": node.GetIsActive(),
|
||||
"uniqueId": node.GetUniqueId(),
|
||||
"secret": node.GetSecret(),
|
||||
"installDir": installDir,
|
||||
"status": statusMap,
|
||||
"installStatus": installStatusMap,
|
||||
"cluster": clusterMap,
|
||||
"login": loginMap,
|
||||
"apiNodeAddrs": []string{},
|
||||
"ipAddresses": ipAddresses,
|
||||
"cluster": clusterMap,
|
||||
"login": loginMap,
|
||||
"apiNodeAddrs": []string{},
|
||||
"ipAddresses": ipAddresses,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -165,21 +165,23 @@ func decodeNodeStatus(raw []byte) maps.Map {
|
||||
memText := fmt.Sprintf("%.2f%%", status.MemoryUsage*100)
|
||||
|
||||
return maps.Map{
|
||||
"isActive": status.IsActive,
|
||||
"updatedAt": status.UpdatedAt,
|
||||
"hostname": status.Hostname,
|
||||
"hostIP": status.HostIP,
|
||||
"cpuUsage": status.CPUUsage,
|
||||
"cpuUsageText": cpuText,
|
||||
"memUsage": status.MemoryUsage,
|
||||
"memUsageText": memText,
|
||||
"load1m": status.Load1m,
|
||||
"load5m": status.Load5m,
|
||||
"load15m": status.Load15m,
|
||||
"buildVersion": status.BuildVersion,
|
||||
"cpuPhysicalCount": status.CPUPhysicalCount,
|
||||
"cpuLogicalCount": status.CPULogicalCount,
|
||||
"exePath": status.ExePath,
|
||||
"isActive": status.IsActive,
|
||||
"updatedAt": status.UpdatedAt,
|
||||
"os": status.OS,
|
||||
"arch": status.Arch,
|
||||
"hostname": status.Hostname,
|
||||
"hostIP": status.HostIP,
|
||||
"cpuUsage": status.CPUUsage,
|
||||
"cpuUsageText": cpuText,
|
||||
"memUsage": status.MemoryUsage,
|
||||
"memUsageText": memText,
|
||||
"load1m": status.Load1m,
|
||||
"load5m": status.Load5m,
|
||||
"load15m": status.Load15m,
|
||||
"buildVersion": status.BuildVersion,
|
||||
"cpuPhysicalCount": status.CPUPhysicalCount,
|
||||
"cpuLogicalCount": status.CPULogicalCount,
|
||||
"exePath": status.ExePath,
|
||||
"apiSuccessPercent": status.APISuccessPercent,
|
||||
"apiAvgCostSeconds": status.APIAvgCostSeconds,
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ func (this *UpdateAction) RunPost(params struct {
|
||||
|
||||
installDir := strings.TrimSpace(node.GetInstallDir())
|
||||
if len(installDir) == 0 {
|
||||
installDir = "/opt/edge-httpdns"
|
||||
installDir = "/root/edge-httpdns"
|
||||
}
|
||||
|
||||
_, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNode(this.AdminContext(), &pb.UpdateHTTPDNSNodeRequest{
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package clusters
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
type ClusterSettingsAction struct {
|
||||
@@ -41,14 +44,15 @@ func (this *ClusterSettingsAction) RunGet(params struct {
|
||||
}
|
||||
|
||||
settings := maps.Map{
|
||||
"name": cluster.GetString("name"),
|
||||
"gatewayDomain": cluster.GetString("gatewayDomain"),
|
||||
"cacheTtl": cluster.GetInt("defaultTTL"),
|
||||
"fallbackTimeout": cluster.GetInt("fallbackTimeout"),
|
||||
"installDir": cluster.GetString("installDir"),
|
||||
"isOn": cluster.GetBool("isOn"),
|
||||
"autoRemoteStart": cluster.GetBool("autoRemoteStart"),
|
||||
"accessLogIsOn": cluster.GetBool("accessLogIsOn"),
|
||||
"name": cluster.GetString("name"),
|
||||
"gatewayDomain": cluster.GetString("gatewayDomain"),
|
||||
"cacheTtl": cluster.GetInt("defaultTTL"),
|
||||
"fallbackTimeout": cluster.GetInt("fallbackTimeout"),
|
||||
"installDir": cluster.GetString("installDir"),
|
||||
"isOn": cluster.GetBool("isOn"),
|
||||
"autoRemoteStart": cluster.GetBool("autoRemoteStart"),
|
||||
"accessLogIsOn": cluster.GetBool("accessLogIsOn"),
|
||||
"timeZone": cluster.GetString("timeZone"),
|
||||
}
|
||||
if settings.GetInt("cacheTtl") <= 0 {
|
||||
settings["cacheTtl"] = 60
|
||||
@@ -57,7 +61,10 @@ func (this *ClusterSettingsAction) RunGet(params struct {
|
||||
settings["fallbackTimeout"] = 300
|
||||
}
|
||||
if len(settings.GetString("installDir")) == 0 {
|
||||
settings["installDir"] = "/opt/edge-httpdns"
|
||||
settings["installDir"] = "/root/edge-httpdns"
|
||||
}
|
||||
if len(settings.GetString("timeZone")) == 0 {
|
||||
settings["timeZone"] = "Asia/Shanghai"
|
||||
}
|
||||
|
||||
listenAddresses := []*serverconfigs.NetworkAddressConfig{
|
||||
@@ -101,19 +108,29 @@ func (this *ClusterSettingsAction) RunGet(params struct {
|
||||
"listen": listenAddresses,
|
||||
"sslPolicy": sslPolicy,
|
||||
}
|
||||
|
||||
this.Data["timeZoneGroups"] = nodeconfigs.FindAllTimeZoneGroups()
|
||||
this.Data["timeZoneLocations"] = nodeconfigs.FindAllTimeZoneLocations()
|
||||
timeZoneStr := settings.GetString("timeZone")
|
||||
if len(timeZoneStr) == 0 {
|
||||
timeZoneStr = nodeconfigs.DefaultTimeZoneLocation
|
||||
}
|
||||
this.Data["timeZoneLocation"] = nodeconfigs.FindTimeZoneLocation(timeZoneStr)
|
||||
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *ClusterSettingsAction) RunPost(params struct {
|
||||
ClusterId int64
|
||||
Name string
|
||||
GatewayDomain string
|
||||
CacheTtl int32
|
||||
FallbackTimeout int32
|
||||
InstallDir string
|
||||
IsOn bool
|
||||
ClusterId int64
|
||||
Name string
|
||||
GatewayDomain string
|
||||
CacheTtl int32
|
||||
FallbackTimeout int32
|
||||
InstallDir string
|
||||
IsOn bool
|
||||
AutoRemoteStart bool
|
||||
AccessLogIsOn bool
|
||||
AccessLogIsOn bool
|
||||
TimeZone string
|
||||
|
||||
Addresses []byte
|
||||
SslPolicyJSON []byte
|
||||
@@ -129,6 +146,31 @@ func (this *ClusterSettingsAction) RunPost(params struct {
|
||||
params.Must.Field("name", params.Name).Require("请输入集群名称")
|
||||
params.Must.Field("gatewayDomain", params.GatewayDomain).Require("请输入服务域名")
|
||||
|
||||
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 开关项按请求值强制覆盖:未提交/空值都视为 false,支持取消勾选
|
||||
autoRemoteStartRaw := strings.ToLower(strings.TrimSpace(this.ParamString("autoRemoteStart")))
|
||||
params.AutoRemoteStart = autoRemoteStartRaw == "1" || autoRemoteStartRaw == "true" || autoRemoteStartRaw == "on" || autoRemoteStartRaw == "yes" || autoRemoteStartRaw == "enabled"
|
||||
|
||||
accessLogIsOnRaw := strings.ToLower(strings.TrimSpace(this.ParamString("accessLogIsOn")))
|
||||
params.AccessLogIsOn = accessLogIsOnRaw == "1" || accessLogIsOnRaw == "true" || accessLogIsOnRaw == "on" || accessLogIsOnRaw == "yes" || accessLogIsOnRaw == "enabled"
|
||||
|
||||
isOnRaw := strings.ToLower(strings.TrimSpace(this.ParamString("isOn")))
|
||||
params.IsOn = isOnRaw == "1" || isOnRaw == "true" || isOnRaw == "on" || isOnRaw == "yes" || isOnRaw == "enabled"
|
||||
|
||||
// 时区为空时继承当前值,再兜底默认值
|
||||
params.TimeZone = strings.TrimSpace(this.ParamString("timeZone"))
|
||||
if len(params.TimeZone) == 0 {
|
||||
params.TimeZone = strings.TrimSpace(cluster.GetString("timeZone"))
|
||||
}
|
||||
if len(params.TimeZone) == 0 {
|
||||
params.TimeZone = "Asia/Shanghai"
|
||||
}
|
||||
|
||||
if params.CacheTtl <= 0 {
|
||||
params.CacheTtl = 60
|
||||
}
|
||||
@@ -136,20 +178,13 @@ func (this *ClusterSettingsAction) RunPost(params struct {
|
||||
params.FallbackTimeout = 300
|
||||
}
|
||||
if len(params.InstallDir) == 0 {
|
||||
params.InstallDir = "/opt/edge-httpdns"
|
||||
}
|
||||
|
||||
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
params.InstallDir = "/root/edge-httpdns"
|
||||
}
|
||||
|
||||
tlsConfig := maps.Map{}
|
||||
if rawTLS := strings.TrimSpace(cluster.GetString("tlsPolicyJSON")); len(rawTLS) > 0 {
|
||||
_ = json.Unmarshal([]byte(rawTLS), &tlsConfig)
|
||||
}
|
||||
|
||||
if len(params.Addresses) > 0 {
|
||||
var addresses []*serverconfigs.NetworkAddressConfig
|
||||
if err := json.Unmarshal(params.Addresses, &addresses); err != nil {
|
||||
@@ -158,7 +193,6 @@ func (this *ClusterSettingsAction) RunPost(params struct {
|
||||
}
|
||||
tlsConfig["listen"] = addresses
|
||||
}
|
||||
|
||||
if len(params.SslPolicyJSON) > 0 {
|
||||
sslPolicy := &sslconfigs.SSLPolicy{}
|
||||
if err := json.Unmarshal(params.SslPolicyJSON, sslPolicy); err != nil {
|
||||
@@ -177,7 +211,7 @@ func (this *ClusterSettingsAction) RunPost(params struct {
|
||||
}
|
||||
}
|
||||
|
||||
_, err = this.RPC().HTTPDNSClusterRPC().UpdateHTTPDNSCluster(this.AdminContext(), &pb.UpdateHTTPDNSClusterRequest{
|
||||
updateReq := &pb.UpdateHTTPDNSClusterRequest{
|
||||
ClusterId: params.ClusterId,
|
||||
Name: params.Name,
|
||||
ServiceDomain: params.GatewayDomain,
|
||||
@@ -189,7 +223,16 @@ func (this *ClusterSettingsAction) RunPost(params struct {
|
||||
IsDefault: false,
|
||||
AutoRemoteStart: params.AutoRemoteStart,
|
||||
AccessLogIsOn: params.AccessLogIsOn,
|
||||
})
|
||||
TimeZone: params.TimeZone,
|
||||
}
|
||||
updateCtx := metadata.AppendToOutgoingContext(
|
||||
this.AdminContext(),
|
||||
"x-httpdns-auto-remote-start", fmt.Sprintf("%t", updateReq.GetAutoRemoteStart()),
|
||||
"x-httpdns-access-log-is-on", fmt.Sprintf("%t", updateReq.GetAccessLogIsOn()),
|
||||
"x-httpdns-time-zone", updateReq.GetTimeZone(),
|
||||
)
|
||||
|
||||
_, err = this.RPC().HTTPDNSClusterRPC().UpdateHTTPDNSCluster(updateCtx, updateReq)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
@@ -36,7 +37,7 @@ func (this *CreateAction) RunPost(params struct {
|
||||
params.GatewayDomain = strings.TrimSpace(params.GatewayDomain)
|
||||
params.InstallDir = strings.TrimSpace(params.InstallDir)
|
||||
if len(params.InstallDir) == 0 {
|
||||
params.InstallDir = "/opt/edge-httpdns"
|
||||
params.InstallDir = "/root/edge-httpdns"
|
||||
}
|
||||
if params.CacheTtl <= 0 {
|
||||
params.CacheTtl = 60
|
||||
@@ -56,6 +57,9 @@ func (this *CreateAction) RunPost(params struct {
|
||||
InstallDir: params.InstallDir,
|
||||
IsOn: params.IsOn,
|
||||
IsDefault: false,
|
||||
AutoRemoteStart: true,
|
||||
AccessLogIsOn: true,
|
||||
TimeZone: "Asia/Shanghai",
|
||||
})
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
@@ -63,5 +67,12 @@ func (this *CreateAction) RunPost(params struct {
|
||||
}
|
||||
|
||||
this.Data["clusterId"] = resp.GetClusterId()
|
||||
|
||||
// fallback: if frontend JS doesn't intercept form submit, redirect instead of showing raw JSON
|
||||
if len(this.Request.Header.Get("X-Requested-With")) == 0 {
|
||||
this.RedirectURL("/httpdns/clusters/cluster?clusterId=" + strconv.FormatInt(resp.GetClusterId(), 10))
|
||||
return
|
||||
}
|
||||
|
||||
this.Success()
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (this *CreateNodeAction) RunPost(params struct {
|
||||
params.InstallDir = strings.TrimSpace(cluster.GetString("installDir"))
|
||||
}
|
||||
if len(params.InstallDir) == 0 {
|
||||
params.InstallDir = "/opt/edge-httpdns"
|
||||
params.InstallDir = "/root/edge-httpdns"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ func init() {
|
||||
// Node level
|
||||
GetPost("/createNode", new(CreateNodeAction)).
|
||||
Post("/deleteNode", new(DeleteNodeAction)).
|
||||
Get("/upgradeRemote", new(UpgradeRemoteAction)).
|
||||
GetPost("/cluster/upgradeRemote", new(UpgradeRemoteAction)).
|
||||
GetPost("/upgradeRemote", new(UpgradeRemoteAction)).
|
||||
Post("/upgradeStatus", new(UpgradeStatusAction)).
|
||||
GetPost("/updateNodeSSH", new(UpdateNodeSSHAction)).
|
||||
Post("/checkPorts", new(CheckPortsAction)).
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func listClusterMaps(parent *actionutils.ParentAction, keyword string) ([]maps.Map, error) {
|
||||
@@ -37,6 +41,11 @@ func listClusterMaps(parent *actionutils.ParentAction, keyword string) ([]maps.M
|
||||
}
|
||||
}
|
||||
|
||||
countUpgradeNodes, err := countUpgradeHTTPDNSNodes(parent, cluster.GetId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
port := "443"
|
||||
if rawTLS := cluster.GetTlsPolicyJSON(); len(rawTLS) > 0 {
|
||||
tlsConfig := maps.Map{}
|
||||
@@ -56,32 +65,87 @@ func listClusterMaps(parent *actionutils.ParentAction, keyword string) ([]maps.M
|
||||
apiAddress := "https://" + cluster.GetServiceDomain() + ":" + port
|
||||
|
||||
result = append(result, maps.Map{
|
||||
"id": cluster.GetId(),
|
||||
"name": cluster.GetName(),
|
||||
"gatewayDomain": cluster.GetServiceDomain(),
|
||||
"apiAddress": apiAddress,
|
||||
"defaultTTL": cluster.GetDefaultTTL(),
|
||||
"fallbackTimeout": cluster.GetFallbackTimeoutMs(),
|
||||
"installDir": cluster.GetInstallDir(),
|
||||
"isOn": cluster.GetIsOn(),
|
||||
"isDefault": cluster.GetIsDefault(),
|
||||
"countAllNodes": countAllNodes,
|
||||
"countActiveNodes": countActiveNodes,
|
||||
"id": cluster.GetId(),
|
||||
"name": cluster.GetName(),
|
||||
"gatewayDomain": cluster.GetServiceDomain(),
|
||||
"apiAddress": apiAddress,
|
||||
"defaultTTL": cluster.GetDefaultTTL(),
|
||||
"fallbackTimeout": cluster.GetFallbackTimeoutMs(),
|
||||
"installDir": cluster.GetInstallDir(),
|
||||
"timeZone": cluster.GetTimeZone(),
|
||||
"isOn": cluster.GetIsOn(),
|
||||
"isDefault": cluster.GetIsDefault(),
|
||||
"autoRemoteStart": cluster.GetAutoRemoteStart(),
|
||||
"accessLogIsOn": cluster.GetAccessLogIsOn(),
|
||||
"tlsPolicyJSON": cluster.GetTlsPolicyJSON(),
|
||||
"countAllNodes": countAllNodes,
|
||||
"countActiveNodes": countActiveNodes,
|
||||
"countUpgradeNodes": countUpgradeNodes,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func countUpgradeHTTPDNSNodes(parent *actionutils.ParentAction, clusterID int64) (int64, error) {
|
||||
countResp, err := parent.RPC().HTTPDNSNodeRPC().CountAllUpgradeHTTPDNSNodesWithClusterId(parent.AdminContext(), &pb.CountAllUpgradeHTTPDNSNodesWithClusterIdRequest{
|
||||
ClusterId: clusterID,
|
||||
})
|
||||
if err == nil {
|
||||
return countResp.GetCount(), nil
|
||||
}
|
||||
|
||||
grpcStatus, ok := status.FromError(err)
|
||||
if !ok || grpcStatus.Code() != codes.Unimplemented {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Compatibility fallback: old edge-api may not implement countAllUpgradeHTTPDNSNodesWithClusterId yet.
|
||||
listResp, listErr := parent.RPC().HTTPDNSNodeRPC().FindAllUpgradeHTTPDNSNodesWithClusterId(parent.AdminContext(), &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest{
|
||||
ClusterId: clusterID,
|
||||
})
|
||||
if listErr == nil {
|
||||
return int64(len(listResp.GetNodes())), nil
|
||||
}
|
||||
|
||||
listStatus, ok := status.FromError(listErr)
|
||||
if ok && listStatus.Code() == codes.Unimplemented {
|
||||
// Compatibility fallback: both methods missing on old edge-api, don't block page rendering.
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return 0, listErr
|
||||
}
|
||||
|
||||
func findClusterMap(parent *actionutils.ParentAction, clusterID int64) (maps.Map, error) {
|
||||
if clusterID > 0 {
|
||||
var headerMD metadata.MD
|
||||
resp, err := parent.RPC().HTTPDNSClusterRPC().FindHTTPDNSCluster(parent.AdminContext(), &pb.FindHTTPDNSClusterRequest{
|
||||
ClusterId: clusterID,
|
||||
})
|
||||
}, grpc.Header(&headerMD))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.GetCluster() != nil {
|
||||
cluster := resp.GetCluster()
|
||||
autoRemoteStart := cluster.GetAutoRemoteStart()
|
||||
accessLogIsOn := cluster.GetAccessLogIsOn()
|
||||
timeZone := cluster.GetTimeZone()
|
||||
|
||||
// Compatibility fallback:
|
||||
// Some deployed admin binaries may decode newly-added protobuf fields incorrectly.
|
||||
// Read values from grpc response headers as a source of truth.
|
||||
if values := headerMD.Get("x-httpdns-auto-remote-start"); len(values) > 0 {
|
||||
autoRemoteStart = parseBoolLike(values[0], autoRemoteStart)
|
||||
}
|
||||
if values := headerMD.Get("x-httpdns-access-log-is-on"); len(values) > 0 {
|
||||
accessLogIsOn = parseBoolLike(values[0], accessLogIsOn)
|
||||
}
|
||||
if values := headerMD.Get("x-httpdns-time-zone"); len(values) > 0 {
|
||||
if tz := strings.TrimSpace(values[0]); len(tz) > 0 {
|
||||
timeZone = tz
|
||||
}
|
||||
}
|
||||
|
||||
return maps.Map{
|
||||
"id": cluster.GetId(),
|
||||
"name": cluster.GetName(),
|
||||
@@ -92,8 +156,9 @@ func findClusterMap(parent *actionutils.ParentAction, clusterID int64) (maps.Map
|
||||
"isOn": cluster.GetIsOn(),
|
||||
"isDefault": cluster.GetIsDefault(),
|
||||
"tlsPolicyJSON": cluster.GetTlsPolicyJSON(),
|
||||
"autoRemoteStart": cluster.GetAutoRemoteStart(),
|
||||
"accessLogIsOn": cluster.GetAccessLogIsOn(),
|
||||
"autoRemoteStart": autoRemoteStart,
|
||||
"accessLogIsOn": accessLogIsOn,
|
||||
"timeZone": timeZone,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -109,12 +174,25 @@ func findClusterMap(parent *actionutils.ParentAction, clusterID int64) (maps.Map
|
||||
"gatewayDomain": "",
|
||||
"defaultTTL": 60,
|
||||
"fallbackTimeout": 300,
|
||||
"installDir": "/opt/edge-httpdns",
|
||||
"installDir": "/root/edge-httpdns",
|
||||
"timeZone": "Asia/Shanghai",
|
||||
}, nil
|
||||
}
|
||||
return clusters[0], nil
|
||||
}
|
||||
|
||||
func parseBoolLike(raw string, defaultValue bool) bool {
|
||||
s := strings.ToLower(strings.TrimSpace(raw))
|
||||
switch s {
|
||||
case "1", "true", "on", "yes", "enabled":
|
||||
return true
|
||||
case "0", "false", "off", "no", "disabled":
|
||||
return false
|
||||
default:
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
func listNodeMaps(parent *actionutils.ParentAction, clusterID int64) ([]maps.Map, error) {
|
||||
resp, err := parent.RPC().HTTPDNSNodeRPC().ListHTTPDNSNodes(parent.AdminContext(), &pb.ListHTTPDNSNodesRequest{
|
||||
ClusterId: clusterID,
|
||||
@@ -132,21 +210,21 @@ func listNodeMaps(parent *actionutils.ParentAction, clusterID int64) ([]maps.Map
|
||||
ip = parsed
|
||||
}
|
||||
nodeMap := maps.Map{
|
||||
"id": node.GetId(),
|
||||
"clusterId": node.GetClusterId(),
|
||||
"name": node.GetName(),
|
||||
"isOn": node.GetIsOn(),
|
||||
"isUp": node.GetIsUp(),
|
||||
"isInstalled": node.GetIsInstalled(),
|
||||
"isActive": node.GetIsActive(),
|
||||
"installDir": node.GetInstallDir(),
|
||||
"uniqueId": node.GetUniqueId(),
|
||||
"secret": node.GetSecret(),
|
||||
"status": statusMap,
|
||||
"id": node.GetId(),
|
||||
"clusterId": node.GetClusterId(),
|
||||
"name": node.GetName(),
|
||||
"isOn": node.GetIsOn(),
|
||||
"isUp": node.GetIsUp(),
|
||||
"isInstalled": node.GetIsInstalled(),
|
||||
"isActive": node.GetIsActive(),
|
||||
"installDir": node.GetInstallDir(),
|
||||
"uniqueId": node.GetUniqueId(),
|
||||
"secret": node.GetSecret(),
|
||||
"status": statusMap,
|
||||
"installStatus": installStatusMap,
|
||||
"region": nil,
|
||||
"login": nil,
|
||||
"apiNodeAddrs": []string{},
|
||||
"region": nil,
|
||||
"login": nil,
|
||||
"apiNodeAddrs": []string{},
|
||||
"cluster": maps.Map{
|
||||
"id": node.GetClusterId(),
|
||||
"installDir": node.GetInstallDir(),
|
||||
@@ -227,21 +305,21 @@ func decodeNodeStatus(raw []byte) maps.Map {
|
||||
cpuText := fmt.Sprintf("%.2f%%", status.CPUUsage*100)
|
||||
memText := fmt.Sprintf("%.2f%%", status.MemoryUsage*100)
|
||||
return maps.Map{
|
||||
"isActive": status.IsActive,
|
||||
"updatedAt": status.UpdatedAt,
|
||||
"hostname": status.Hostname,
|
||||
"hostIP": status.HostIP,
|
||||
"cpuUsage": status.CPUUsage,
|
||||
"cpuUsageText": cpuText,
|
||||
"memUsage": status.MemoryUsage,
|
||||
"memUsageText": memText,
|
||||
"load1m": status.Load1m,
|
||||
"load5m": status.Load5m,
|
||||
"load15m": status.Load15m,
|
||||
"buildVersion": status.BuildVersion,
|
||||
"isActive": status.IsActive,
|
||||
"updatedAt": status.UpdatedAt,
|
||||
"hostname": status.Hostname,
|
||||
"hostIP": status.HostIP,
|
||||
"cpuUsage": status.CPUUsage,
|
||||
"cpuUsageText": cpuText,
|
||||
"memUsage": status.MemoryUsage,
|
||||
"memUsageText": memText,
|
||||
"load1m": status.Load1m,
|
||||
"load5m": status.Load5m,
|
||||
"load15m": status.Load15m,
|
||||
"buildVersion": status.BuildVersion,
|
||||
"cpuPhysicalCount": status.CPUPhysicalCount,
|
||||
"cpuLogicalCount": status.CPULogicalCount,
|
||||
"exePath": status.ExePath,
|
||||
"cpuLogicalCount": status.CPULogicalCount,
|
||||
"exePath": status.ExePath,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,108 @@
|
||||
package clusters
|
||||
|
||||
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type UpgradeRemoteAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *UpgradeRemoteAction) Init() {
|
||||
this.Nav("httpdns", "cluster", "index")
|
||||
}
|
||||
|
||||
func (this *UpgradeRemoteAction) RunGet(params struct {
|
||||
NodeId int64
|
||||
ClusterId int64
|
||||
}) {
|
||||
this.Data["nodeId"] = params.NodeId
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
|
||||
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
httpdnsutils.AddClusterTabbar(this.Parent(), cluster.GetString("name"), params.ClusterId, "node")
|
||||
|
||||
this.Data["clusterId"] = params.ClusterId
|
||||
this.Data["cluster"] = cluster
|
||||
|
||||
resp, err := this.RPC().HTTPDNSNodeRPC().FindAllUpgradeHTTPDNSNodesWithClusterId(this.AdminContext(), &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest{
|
||||
ClusterId: params.ClusterId,
|
||||
})
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
|
||||
nodes := make([]maps.Map, 0, len(resp.GetNodes()))
|
||||
for _, upgradeNode := range resp.GetNodes() {
|
||||
node := upgradeNode.Node
|
||||
if node == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
loginParams := maps.Map{}
|
||||
if node.GetNodeLogin() != nil && len(node.GetNodeLogin().GetParams()) > 0 {
|
||||
_ = json.Unmarshal(node.GetNodeLogin().GetParams(), &loginParams)
|
||||
}
|
||||
|
||||
status := decodeNodeStatus(node.GetStatusJSON())
|
||||
accessIP := strings.TrimSpace(status.GetString("hostIP"))
|
||||
if len(accessIP) == 0 {
|
||||
accessIP = strings.TrimSpace(node.GetName())
|
||||
}
|
||||
|
||||
nodes = append(nodes, maps.Map{
|
||||
"id": node.GetId(),
|
||||
"name": node.GetName(),
|
||||
"accessIP": accessIP,
|
||||
"oldVersion": upgradeNode.OldVersion,
|
||||
"newVersion": upgradeNode.NewVersion,
|
||||
"login": node.GetNodeLogin(),
|
||||
"loginParams": loginParams,
|
||||
"installStatus": decodeUpgradeInstallStatus(node.GetInstallStatusJSON()),
|
||||
})
|
||||
}
|
||||
this.Data["nodes"] = nodes
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *UpgradeRemoteAction) RunPost(params struct {
|
||||
NodeId int64
|
||||
|
||||
Must *actions.Must
|
||||
}) {
|
||||
_, err := this.RPC().HTTPDNSNodeRPC().UpgradeHTTPDNSNode(this.AdminContext(), &pb.UpgradeHTTPDNSNodeRequest{
|
||||
NodeId: params.NodeId,
|
||||
})
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
this.Success()
|
||||
}
|
||||
|
||||
func decodeUpgradeInstallStatus(raw []byte) maps.Map {
|
||||
result := maps.Map{
|
||||
"isRunning": false,
|
||||
"isFinished": false,
|
||||
"isOk": false,
|
||||
"error": "",
|
||||
"errorCode": "",
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
_ = json.Unmarshal(raw, &result)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
)
|
||||
|
||||
type UpgradeStatusAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *UpgradeStatusAction) RunPost(params struct {
|
||||
NodeId int64
|
||||
}) {
|
||||
resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
|
||||
NodeId: params.NodeId,
|
||||
})
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
if resp.GetNode() == nil {
|
||||
this.Data["status"] = nil
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
this.Data["status"] = decodeUpgradeInstallStatusMap(resp.GetNode().GetInstallStatusJSON())
|
||||
this.Success()
|
||||
}
|
||||
|
||||
func decodeUpgradeInstallStatusMap(raw []byte) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"isRunning": false,
|
||||
"isFinished": false,
|
||||
"isOk": false,
|
||||
"error": "",
|
||||
"errorCode": "",
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
_ = json.Unmarshal(raw, &result)
|
||||
return result
|
||||
}
|
||||
@@ -39,6 +39,7 @@ func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool)
|
||||
if configloaders.AllowModule(adminId, configloaders.AdminModuleCodeSetting) {
|
||||
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabAdminServer), "", "/settings/server", "", this.tab == "server")
|
||||
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabAdminUI), "", "/settings/ui", "", this.tab == "ui")
|
||||
tabbar.Add("升级设置", "", "/settings/upgrade", "", this.tab == "upgrade")
|
||||
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabAdminSecuritySettings), "", "/settings/security", "", this.tab == "security")
|
||||
if teaconst.IsPlus {
|
||||
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabIPLibrary), "", "/settings/ip-library", "", this.tab == "ipLibrary")
|
||||
|
||||
@@ -42,6 +42,7 @@ func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool)
|
||||
if teaconst.IsPlus {
|
||||
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabUserUI), "", "/settings/user-ui", "", this.tab == "userUI")
|
||||
}
|
||||
tabbar.Add("升级设置", "", "/settings/upgrade", "", this.tab == "upgrade")
|
||||
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabAdminSecuritySettings), "", "/settings/security", "", this.tab == "security")
|
||||
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabIPLibrary), "", "/settings/ip-library", "", this.tab == "ipLibrary")
|
||||
}
|
||||
|
||||
@@ -56,9 +56,6 @@ func (this *IndexAction) RunPost(params struct {
|
||||
TimeZone string
|
||||
DnsResolverType string
|
||||
|
||||
SupportModuleCDN bool
|
||||
SupportModuleNS bool
|
||||
|
||||
Must *actions.Must
|
||||
CSRF *actionutils.CSRF
|
||||
}) {
|
||||
@@ -93,13 +90,7 @@ func (this *IndexAction) RunPost(params struct {
|
||||
config.DefaultPageSize = 10
|
||||
}
|
||||
|
||||
config.Modules = []userconfigs.UserModule{}
|
||||
if params.SupportModuleCDN {
|
||||
config.Modules = append(config.Modules, userconfigs.UserModuleCDN)
|
||||
}
|
||||
if params.SupportModuleNS {
|
||||
config.Modules = append(config.Modules, userconfigs.UserModuleNS)
|
||||
}
|
||||
config.Modules = []userconfigs.UserModule{userconfigs.UserModuleCDN, userconfigs.UserModuleNS}
|
||||
|
||||
// 上传Favicon文件
|
||||
if params.FaviconFile != nil {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//go:build !plus
|
||||
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
// loadDNSUpgradeModules 非Plus版本不支持DNS模块
|
||||
func loadDNSUpgradeModules(parent *actionutils.ParentAction) []maps.Map {
|
||||
return nil
|
||||
}
|
||||
|
||||
// upgradeDNSNode 非Plus版本不支持DNS节点升级
|
||||
func upgradeDNSNode(parent *actionutils.ParentAction, nodeId int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadDNSNodeStatus 非Plus版本不支持DNS节点状态查询
|
||||
func loadDNSNodeStatus(parent *actionutils.ParentAction, nodeIds []int64) []maps.Map {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
//go:build plus
|
||||
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
// loadDNSUpgradeModules 加载DNS模块的待升级节点信息
|
||||
func loadDNSUpgradeModules(parent *actionutils.ParentAction) []maps.Map {
|
||||
clustersResp, err := parent.RPC().NSClusterRPC().ListNSClusters(parent.AdminContext(), &pb.ListNSClustersRequest{
|
||||
Offset: 0,
|
||||
Size: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var clusterMaps []maps.Map
|
||||
for _, cluster := range clustersResp.NsClusters {
|
||||
nodesResp, err := parent.RPC().NSNodeRPC().FindAllUpgradeNSNodesWithNSClusterId(parent.AdminContext(), &pb.FindAllUpgradeNSNodesWithNSClusterIdRequest{
|
||||
NsClusterId: cluster.Id,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var nodeMaps []maps.Map
|
||||
for _, nodeUpgrade := range nodesResp.Nodes {
|
||||
if nodeUpgrade.NsNode == nil {
|
||||
continue
|
||||
}
|
||||
installStatusMap := decodeInstallStatusFromPB(nodeUpgrade.NsNode.InstallStatus)
|
||||
accessIP, login, loginParams := decodeNodeAccessInfo(nodeUpgrade.NsNode.StatusJSON, nodeUpgrade.NsNode.NodeLogin, nodeUpgrade.NsNode.Name)
|
||||
|
||||
nodeMaps = append(nodeMaps, maps.Map{
|
||||
"id": nodeUpgrade.NsNode.Id,
|
||||
"name": nodeUpgrade.NsNode.Name,
|
||||
"os": nodeUpgrade.Os,
|
||||
"arch": nodeUpgrade.Arch,
|
||||
"oldVersion": nodeUpgrade.OldVersion,
|
||||
"newVersion": nodeUpgrade.NewVersion,
|
||||
"isOn": nodeUpgrade.NsNode.IsOn,
|
||||
"isUp": nodeUpgrade.NsNode.IsUp,
|
||||
"accessIP": accessIP,
|
||||
"login": login,
|
||||
"loginParams": loginParams,
|
||||
"installStatus": installStatusMap,
|
||||
})
|
||||
}
|
||||
if len(nodeMaps) == 0 {
|
||||
continue
|
||||
}
|
||||
clusterMaps = append(clusterMaps, maps.Map{
|
||||
"id": cluster.Id,
|
||||
"name": cluster.Name,
|
||||
"nodes": nodeMaps,
|
||||
"count": len(nodeMaps),
|
||||
})
|
||||
}
|
||||
return clusterMaps
|
||||
}
|
||||
|
||||
// upgradeDNSNode 升级DNS节点
|
||||
func upgradeDNSNode(parent *actionutils.ParentAction, nodeId int64) error {
|
||||
_, err := parent.RPC().NSNodeRPC().UpgradeNSNode(parent.AdminContext(), &pb.UpgradeNSNodeRequest{
|
||||
NsNodeId: nodeId,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// loadDNSNodeStatus 加载DNS节点安装状态
|
||||
func loadDNSNodeStatus(parent *actionutils.ParentAction, nodeIds []int64) []maps.Map {
|
||||
var result []maps.Map
|
||||
for _, nodeId := range nodeIds {
|
||||
resp, err := parent.RPC().NSNodeRPC().FindNSNode(parent.AdminContext(), &pb.FindNSNodeRequest{
|
||||
NsNodeId: nodeId,
|
||||
})
|
||||
if err != nil || resp.NsNode == nil {
|
||||
continue
|
||||
}
|
||||
var installStatusMap maps.Map
|
||||
if resp.NsNode.InstallStatus != nil {
|
||||
installStatusMap = maps.Map{
|
||||
"isRunning": resp.NsNode.InstallStatus.IsRunning,
|
||||
"isFinished": resp.NsNode.InstallStatus.IsFinished,
|
||||
"isOk": resp.NsNode.InstallStatus.IsOk,
|
||||
"error": resp.NsNode.InstallStatus.Error,
|
||||
"errorCode": resp.NsNode.InstallStatus.ErrorCode,
|
||||
"updatedAt": resp.NsNode.InstallStatus.UpdatedAt,
|
||||
}
|
||||
}
|
||||
result = append(result, maps.Map{
|
||||
"id": nodeId,
|
||||
"installStatus": installStatusMap,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,6 +1,15 @@
|
||||
package upgrade
|
||||
|
||||
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
@@ -11,5 +20,264 @@ func (this *IndexAction) Init() {
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct{}) {
|
||||
// 加载升级配置
|
||||
config, err := configloaders.LoadUpgradeConfig()
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
this.Data["config"] = config
|
||||
|
||||
// 模块列表
|
||||
modules := []maps.Map{}
|
||||
|
||||
// 1. 边缘节点 (EdgeNode)
|
||||
nodeModule := this.loadEdgeNodeModule()
|
||||
if nodeModule != nil {
|
||||
modules = append(modules, nodeModule)
|
||||
}
|
||||
|
||||
// 2. DNS节点 (EdgeDNS) — 仅Plus版本
|
||||
if teaconst.IsPlus {
|
||||
dnsClusters := loadDNSUpgradeModules(&this.ParentAction)
|
||||
if len(dnsClusters) > 0 {
|
||||
totalCount := 0
|
||||
for _, c := range dnsClusters {
|
||||
totalCount += c.GetInt("count")
|
||||
}
|
||||
modules = append(modules, maps.Map{
|
||||
"name": "DNS节点",
|
||||
"code": "dns",
|
||||
"clusters": dnsClusters,
|
||||
"count": totalCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 3. HTTPDNS节点
|
||||
httpdnsModule := this.loadHTTPDNSModule()
|
||||
if httpdnsModule != nil {
|
||||
modules = append(modules, httpdnsModule)
|
||||
}
|
||||
|
||||
this.Data["modules"] = modules
|
||||
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunPost(params struct {
|
||||
AutoUpgrade bool
|
||||
}) {
|
||||
config, err := configloaders.LoadUpgradeConfig()
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
|
||||
config.AutoUpgrade = params.AutoUpgrade
|
||||
|
||||
err = configloaders.UpdateUpgradeConfig(config)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
|
||||
this.Success()
|
||||
}
|
||||
|
||||
// loadEdgeNodeModule 加载边缘节点模块的待升级信息
|
||||
func (this *IndexAction) loadEdgeNodeModule() maps.Map {
|
||||
// 获取所有集群
|
||||
clustersResp, err := this.RPC().NodeClusterRPC().ListEnabledNodeClusters(this.AdminContext(), &pb.ListEnabledNodeClustersRequest{
|
||||
Offset: 0,
|
||||
Size: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var clusterMaps []maps.Map
|
||||
totalCount := 0
|
||||
for _, cluster := range clustersResp.NodeClusters {
|
||||
resp, err := this.RPC().NodeRPC().FindAllUpgradeNodesWithNodeClusterId(this.AdminContext(), &pb.FindAllUpgradeNodesWithNodeClusterIdRequest{
|
||||
NodeClusterId: cluster.Id,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(resp.Nodes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var nodeMaps []maps.Map
|
||||
for _, nodeUpgrade := range resp.Nodes {
|
||||
if nodeUpgrade.Node == nil {
|
||||
continue
|
||||
}
|
||||
installStatusMap := decodeInstallStatusFromPB(nodeUpgrade.Node.InstallStatus)
|
||||
accessIP, login, loginParams := decodeNodeAccessInfo(nodeUpgrade.Node.StatusJSON, nodeUpgrade.Node.NodeLogin, nodeUpgrade.Node.Name)
|
||||
|
||||
nodeMaps = append(nodeMaps, maps.Map{
|
||||
"id": nodeUpgrade.Node.Id,
|
||||
"name": nodeUpgrade.Node.Name,
|
||||
"os": nodeUpgrade.Os,
|
||||
"arch": nodeUpgrade.Arch,
|
||||
"oldVersion": nodeUpgrade.OldVersion,
|
||||
"newVersion": nodeUpgrade.NewVersion,
|
||||
"isOn": nodeUpgrade.Node.IsOn,
|
||||
"isUp": nodeUpgrade.Node.IsUp,
|
||||
"accessIP": accessIP,
|
||||
"login": login,
|
||||
"loginParams": loginParams,
|
||||
"installStatus": installStatusMap,
|
||||
})
|
||||
}
|
||||
totalCount += len(nodeMaps)
|
||||
clusterMaps = append(clusterMaps, maps.Map{
|
||||
"id": cluster.Id,
|
||||
"name": cluster.Name,
|
||||
"nodes": nodeMaps,
|
||||
"count": len(nodeMaps),
|
||||
})
|
||||
}
|
||||
|
||||
if len(clusterMaps) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return maps.Map{
|
||||
"name": "边缘节点",
|
||||
"code": "node",
|
||||
"clusters": clusterMaps,
|
||||
"count": totalCount,
|
||||
}
|
||||
}
|
||||
|
||||
// loadHTTPDNSModule 加载HTTPDNS模块的待升级信息
|
||||
func (this *IndexAction) loadHTTPDNSModule() maps.Map {
|
||||
clustersResp, err := this.RPC().HTTPDNSClusterRPC().ListHTTPDNSClusters(this.AdminContext(), &pb.ListHTTPDNSClustersRequest{
|
||||
Offset: 0,
|
||||
Size: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var clusterMaps []maps.Map
|
||||
totalCount := 0
|
||||
for _, cluster := range clustersResp.Clusters {
|
||||
resp, err := this.RPC().HTTPDNSNodeRPC().FindAllUpgradeHTTPDNSNodesWithClusterId(this.AdminContext(), &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest{
|
||||
ClusterId: cluster.Id,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(resp.Nodes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var nodeMaps []maps.Map
|
||||
for _, nodeUpgrade := range resp.Nodes {
|
||||
if nodeUpgrade.Node == nil {
|
||||
continue
|
||||
}
|
||||
installStatusMap := decodeInstallStatusFromJSON(nodeUpgrade.Node.InstallStatusJSON)
|
||||
accessIP, login, loginParams := decodeNodeAccessInfo(nodeUpgrade.Node.StatusJSON, nodeUpgrade.Node.NodeLogin, nodeUpgrade.Node.Name)
|
||||
|
||||
nodeMaps = append(nodeMaps, maps.Map{
|
||||
"id": nodeUpgrade.Node.Id,
|
||||
"name": nodeUpgrade.Node.Name,
|
||||
"os": nodeUpgrade.Os,
|
||||
"arch": nodeUpgrade.Arch,
|
||||
"oldVersion": nodeUpgrade.OldVersion,
|
||||
"newVersion": nodeUpgrade.NewVersion,
|
||||
"isOn": nodeUpgrade.Node.IsOn,
|
||||
"isUp": nodeUpgrade.Node.IsUp,
|
||||
"accessIP": accessIP,
|
||||
"login": login,
|
||||
"loginParams": loginParams,
|
||||
"installStatus": installStatusMap,
|
||||
})
|
||||
}
|
||||
totalCount += len(nodeMaps)
|
||||
clusterMaps = append(clusterMaps, maps.Map{
|
||||
"id": cluster.Id,
|
||||
"name": cluster.Name,
|
||||
"nodes": nodeMaps,
|
||||
"count": len(nodeMaps),
|
||||
})
|
||||
}
|
||||
|
||||
if len(clusterMaps) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return maps.Map{
|
||||
"name": "HTTPDNS节点",
|
||||
"code": "httpdns",
|
||||
"clusters": clusterMaps,
|
||||
"count": totalCount,
|
||||
}
|
||||
}
|
||||
|
||||
// decodeInstallStatusFromPB 从 protobuf InstallStatus 解码安装状态
|
||||
func decodeInstallStatusFromPB(status *pb.NodeInstallStatus) maps.Map {
|
||||
if status == nil {
|
||||
return nil
|
||||
}
|
||||
// 历史成功状态,在待升级列表中忽略
|
||||
if status.IsFinished && status.IsOk {
|
||||
return nil
|
||||
}
|
||||
return maps.Map{
|
||||
"isRunning": status.IsRunning,
|
||||
"isFinished": status.IsFinished,
|
||||
"isOk": status.IsOk,
|
||||
"error": status.Error,
|
||||
"errorCode": status.ErrorCode,
|
||||
"updatedAt": status.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// decodeInstallStatusFromJSON 从 JSON 字节解码安装状态
|
||||
func decodeInstallStatusFromJSON(raw []byte) maps.Map {
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := maps.Map{}
|
||||
_ = json.Unmarshal(raw, &result)
|
||||
|
||||
isFinished, _ := result["isFinished"].(bool)
|
||||
isOk, _ := result["isOk"].(bool)
|
||||
if isFinished && isOk {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// decodeNodeAccessInfo 从节点状态和登录信息中提取 accessIP、login、loginParams
|
||||
func decodeNodeAccessInfo(statusJSON []byte, nodeLogin *pb.NodeLogin, nodeName string) (accessIP string, login maps.Map, loginParams maps.Map) {
|
||||
// 从 statusJSON 中提取 hostIP 作为 accessIP
|
||||
if len(statusJSON) > 0 {
|
||||
statusMap := maps.Map{}
|
||||
_ = json.Unmarshal(statusJSON, &statusMap)
|
||||
accessIP = strings.TrimSpace(statusMap.GetString("hostIP"))
|
||||
}
|
||||
if len(accessIP) == 0 {
|
||||
accessIP = strings.TrimSpace(nodeName)
|
||||
}
|
||||
|
||||
// 解码 login 信息
|
||||
if nodeLogin != nil {
|
||||
login = maps.Map{
|
||||
"id": nodeLogin.Id,
|
||||
"name": nodeLogin.Name,
|
||||
"type": nodeLogin.Type,
|
||||
}
|
||||
if len(nodeLogin.Params) > 0 {
|
||||
loginParams = maps.Map{}
|
||||
_ = json.Unmarshal(nodeLogin.Params, &loginParams)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ func init() {
|
||||
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeSetting)).
|
||||
Helper(settingutils.NewHelper("upgrade")).
|
||||
Prefix("/settings/upgrade").
|
||||
Get("", new(IndexAction)).
|
||||
GetPost("", new(IndexAction)).
|
||||
Post("/upgradeNode", new(UpgradeNodeAction)).
|
||||
Post("/status", new(StatusAction)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type StatusAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *StatusAction) RunPost(params struct {
|
||||
NodeIdsJSON []byte // JSON: {"node": [1,2], "dns": [3], "httpdns": [4,5]}
|
||||
}) {
|
||||
var nodeIdsMap map[string][]int64
|
||||
if len(params.NodeIdsJSON) > 0 {
|
||||
_ = json.Unmarshal(params.NodeIdsJSON, &nodeIdsMap)
|
||||
}
|
||||
|
||||
result := maps.Map{}
|
||||
|
||||
// EdgeNode 状态
|
||||
if nodeIds, ok := nodeIdsMap["node"]; ok && len(nodeIds) > 0 {
|
||||
var nodeStatuses []maps.Map
|
||||
for _, nodeId := range nodeIds {
|
||||
resp, err := this.RPC().NodeRPC().FindEnabledNode(this.AdminContext(), &pb.FindEnabledNodeRequest{NodeId: nodeId})
|
||||
if err != nil || resp.Node == nil {
|
||||
continue
|
||||
}
|
||||
var installStatusMap maps.Map
|
||||
if resp.Node.InstallStatus != nil {
|
||||
installStatusMap = maps.Map{
|
||||
"isRunning": resp.Node.InstallStatus.IsRunning,
|
||||
"isFinished": resp.Node.InstallStatus.IsFinished,
|
||||
"isOk": resp.Node.InstallStatus.IsOk,
|
||||
"error": resp.Node.InstallStatus.Error,
|
||||
"errorCode": resp.Node.InstallStatus.ErrorCode,
|
||||
"updatedAt": resp.Node.InstallStatus.UpdatedAt,
|
||||
}
|
||||
}
|
||||
nodeStatuses = append(nodeStatuses, maps.Map{
|
||||
"id": nodeId,
|
||||
"installStatus": installStatusMap,
|
||||
})
|
||||
}
|
||||
result["node"] = nodeStatuses
|
||||
}
|
||||
|
||||
// DNS 状态
|
||||
if nodeIds, ok := nodeIdsMap["dns"]; ok && len(nodeIds) > 0 {
|
||||
result["dns"] = loadDNSNodeStatus(&this.ParentAction, nodeIds)
|
||||
}
|
||||
|
||||
// HTTPDNS 状态
|
||||
if nodeIds, ok := nodeIdsMap["httpdns"]; ok && len(nodeIds) > 0 {
|
||||
var nodeStatuses []maps.Map
|
||||
for _, nodeId := range nodeIds {
|
||||
resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{NodeId: nodeId})
|
||||
if err != nil || resp.Node == nil {
|
||||
continue
|
||||
}
|
||||
var installStatusMap maps.Map
|
||||
if len(resp.Node.InstallStatusJSON) > 0 {
|
||||
installStatusMap = maps.Map{}
|
||||
_ = json.Unmarshal(resp.Node.InstallStatusJSON, &installStatusMap)
|
||||
}
|
||||
nodeStatuses = append(nodeStatuses, maps.Map{
|
||||
"id": nodeId,
|
||||
"installStatus": installStatusMap,
|
||||
})
|
||||
}
|
||||
result["httpdns"] = nodeStatuses
|
||||
}
|
||||
|
||||
this.Data["statuses"] = result
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type UpgradeNodeAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *UpgradeNodeAction) RunPost(params struct {
|
||||
Module string // node, dns, httpdns
|
||||
Scope string // all, module, cluster, node
|
||||
ClusterId int64
|
||||
NodeId int64
|
||||
}) {
|
||||
switch params.Scope {
|
||||
case "node":
|
||||
err := this.upgradeSingleNode(params.Module, params.NodeId)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
case "cluster":
|
||||
err := this.upgradeCluster(params.Module, params.ClusterId)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
case "module":
|
||||
err := this.upgradeModule(params.Module)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
case "all":
|
||||
_ = this.upgradeModule("node")
|
||||
_ = this.upgradeModule("dns")
|
||||
_ = this.upgradeModule("httpdns")
|
||||
}
|
||||
|
||||
this.Success()
|
||||
}
|
||||
|
||||
func (this *UpgradeNodeAction) upgradeSingleNode(module string, nodeId int64) error {
|
||||
switch module {
|
||||
case "node":
|
||||
_, err := this.RPC().NodeRPC().UpgradeNode(this.AdminContext(), &pb.UpgradeNodeRequest{NodeId: nodeId})
|
||||
return err
|
||||
case "dns":
|
||||
return upgradeDNSNode(&this.ParentAction, nodeId)
|
||||
case "httpdns":
|
||||
_, err := this.RPC().HTTPDNSNodeRPC().UpgradeHTTPDNSNode(this.AdminContext(), &pb.UpgradeHTTPDNSNodeRequest{NodeId: nodeId})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *UpgradeNodeAction) upgradeCluster(module string, clusterId int64) error {
|
||||
switch module {
|
||||
case "node":
|
||||
resp, err := this.RPC().NodeRPC().FindAllUpgradeNodesWithNodeClusterId(this.AdminContext(), &pb.FindAllUpgradeNodesWithNodeClusterIdRequest{
|
||||
NodeClusterId: clusterId,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, nodeUpgrade := range resp.Nodes {
|
||||
if nodeUpgrade.Node == nil {
|
||||
continue
|
||||
}
|
||||
_, _ = this.RPC().NodeRPC().UpgradeNode(this.AdminContext(), &pb.UpgradeNodeRequest{NodeId: nodeUpgrade.Node.Id})
|
||||
}
|
||||
case "dns":
|
||||
this.upgradeDNSCluster(clusterId)
|
||||
case "httpdns":
|
||||
resp, err := this.RPC().HTTPDNSNodeRPC().FindAllUpgradeHTTPDNSNodesWithClusterId(this.AdminContext(), &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest{
|
||||
ClusterId: clusterId,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, nodeUpgrade := range resp.Nodes {
|
||||
if nodeUpgrade.Node == nil {
|
||||
continue
|
||||
}
|
||||
_, _ = this.RPC().HTTPDNSNodeRPC().UpgradeHTTPDNSNode(this.AdminContext(), &pb.UpgradeHTTPDNSNodeRequest{NodeId: nodeUpgrade.Node.Id})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *UpgradeNodeAction) upgradeModule(module string) error {
|
||||
switch module {
|
||||
case "node":
|
||||
clustersResp, err := this.RPC().NodeClusterRPC().ListEnabledNodeClusters(this.AdminContext(), &pb.ListEnabledNodeClustersRequest{
|
||||
Offset: 0,
|
||||
Size: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, cluster := range clustersResp.NodeClusters {
|
||||
_ = this.upgradeCluster("node", cluster.Id)
|
||||
}
|
||||
case "dns":
|
||||
dnsClusters := loadDNSUpgradeModules(&this.ParentAction)
|
||||
for _, c := range dnsClusters {
|
||||
this.upgradeDNSClusterFromMap(c)
|
||||
}
|
||||
case "httpdns":
|
||||
clustersResp, err := this.RPC().HTTPDNSClusterRPC().ListHTTPDNSClusters(this.AdminContext(), &pb.ListHTTPDNSClustersRequest{
|
||||
Offset: 0,
|
||||
Size: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, cluster := range clustersResp.Clusters {
|
||||
_ = this.upgradeCluster("httpdns", cluster.Id)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// upgradeDNSCluster 根据集群ID升级DNS节点
|
||||
func (this *UpgradeNodeAction) upgradeDNSCluster(clusterId int64) {
|
||||
dnsClusters := loadDNSUpgradeModules(&this.ParentAction)
|
||||
for _, c := range dnsClusters {
|
||||
if c.GetInt64("id") == clusterId {
|
||||
this.upgradeDNSClusterFromMap(c)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// upgradeDNSClusterFromMap 从maps.Map中提取节点ID并升级
|
||||
func (this *UpgradeNodeAction) upgradeDNSClusterFromMap(c maps.Map) {
|
||||
nodesVal := c.Get("nodes")
|
||||
if nodeMaps, ok := nodesVal.([]maps.Map); ok {
|
||||
for _, n := range nodeMaps {
|
||||
nodeId := n.GetInt64("id")
|
||||
if nodeId > 0 {
|
||||
_ = upgradeDNSNode(&this.ParentAction, nodeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,20 +25,16 @@ func (this *CreatePopupAction) Init() {
|
||||
func (this *CreatePopupAction) RunGet(params struct{}) {
|
||||
// 检查是否启用了 HTTPDNS 功能(全局用户注册设置中)
|
||||
var hasHTTPDNSFeature = false
|
||||
var defaultHttpdnsClusterId int64 = 0
|
||||
|
||||
resp, err := this.RPC().SysSettingRPC().ReadSysSetting(this.AdminContext(), &pb.ReadSysSettingRequest{Code: systemconfigs.SettingCodeUserRegisterConfig})
|
||||
if err == nil && len(resp.ValueJSON) > 0 {
|
||||
var config = userconfigs.DefaultUserRegisterConfig()
|
||||
if json.Unmarshal(resp.ValueJSON, config) == nil {
|
||||
hasHTTPDNSFeature = config.HTTPDNSIsOn
|
||||
if len(config.HTTPDNSDefaultClusterIds) > 0 {
|
||||
defaultHttpdnsClusterId = config.HTTPDNSDefaultClusterIds[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
this.Data["hasHTTPDNSFeature"] = hasHTTPDNSFeature
|
||||
this.Data["httpdnsClusterId"] = defaultHttpdnsClusterId
|
||||
this.Data["httpdnsClusterId"] = 0
|
||||
|
||||
// 加载所有 HTTPDNS 集群
|
||||
var httpdnsClusters = []maps.Map{}
|
||||
|
||||
@@ -22046,15 +22046,16 @@ Vue.component("ssl-certs-box", {
|
||||
<span class="grey" v-if="description.length > 0">{{description}}</span>
|
||||
<div class="ui divider" v-if="buttonsVisible()"></div>
|
||||
</div>
|
||||
<div v-if="buttonsVisible()">
|
||||
<div v-if="buttonsVisible()" style="display:flex;align-items:center;gap:0.5rem;flex-wrap:nowrap;white-space:nowrap;">
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<span class="disabled" style="margin:0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
|
||||
|
||||
Vue.component("ssl-certs-view", {
|
||||
props: ["v-certs"],
|
||||
data: function () {
|
||||
@@ -22518,12 +22519,16 @@ Vue.component("ssl-config-box", {
|
||||
<span class="red">选择或上传证书后<span v-if="vProtocol == 'https'">HTTPS</span><span v-if="vProtocol == 'tls'">TLS</span>服务才能生效。</span>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="requestCert()" v-if="vServerId != null && vServerId > 0">申请免费证书</button>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: nowrap; white-space: nowrap;">
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled" style="margin: 0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
<template v-if="vServerId != null && vServerId > 0">
|
||||
<span class="disabled" style="margin: 0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="requestCert()">申请免费证书</button>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -22046,15 +22046,16 @@ Vue.component("ssl-certs-box", {
|
||||
<span class="grey" v-if="description.length > 0">{{description}}</span>
|
||||
<div class="ui divider" v-if="buttonsVisible()"></div>
|
||||
</div>
|
||||
<div v-if="buttonsVisible()">
|
||||
<div v-if="buttonsVisible()" style="display:flex;align-items:center;gap:0.5rem;flex-wrap:nowrap;white-space:nowrap;">
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<span class="disabled" style="margin:0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
|
||||
|
||||
Vue.component("ssl-certs-view", {
|
||||
props: ["v-certs"],
|
||||
data: function () {
|
||||
@@ -22518,12 +22519,16 @@ Vue.component("ssl-config-box", {
|
||||
<span class="red">选择或上传证书后<span v-if="vProtocol == 'https'">HTTPS</span><span v-if="vProtocol == 'tls'">TLS</span>服务才能生效。</span>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="requestCert()" v-if="vServerId != null && vServerId > 0">申请免费证书</button>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: nowrap; white-space: nowrap;">
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled" style="margin: 0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
<template v-if="vServerId != null && vServerId > 0">
|
||||
<span class="disabled" style="margin: 0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="requestCert()">申请免费证书</button>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -160,11 +160,11 @@ Vue.component("ssl-certs-box", {
|
||||
<span class="grey" v-if="description.length > 0">{{description}}</span>
|
||||
<div class="ui divider" v-if="buttonsVisible()"></div>
|
||||
</div>
|
||||
<div v-if="buttonsVisible()">
|
||||
<div v-if="buttonsVisible()" style="display:flex;align-items:center;gap:0.5rem;flex-wrap:nowrap;white-space:nowrap;">
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<span class="disabled" style="margin:0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
})
|
||||
|
||||
@@ -427,12 +427,16 @@ Vue.component("ssl-config-box", {
|
||||
<span class="red">选择或上传证书后<span v-if="vProtocol == 'https'">HTTPS</span><span v-if="vProtocol == 'tls'">TLS</span>服务才能生效。</span>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="requestCert()" v-if="vServerId != null && vServerId > 0">申请免费证书</button>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: nowrap; white-space: nowrap;">
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled" style="margin: 0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
<template v-if="vServerId != null && vServerId > 0">
|
||||
<span class="disabled" style="margin: 0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="requestCert()">申请免费证书</button>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<title>{$ htmlEncode .teaTitle}</title>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
||||
{$if eq .teaFaviconFileId 0}
|
||||
<link rel="shortcut icon" href="/images/favicon.png"/>
|
||||
<link rel="shortcut icon" href="/images/favicon.png" />
|
||||
{$else}
|
||||
<link rel="shortcut icon" href="/ui/image/{$.teaFaviconFileId}"/>
|
||||
<link rel="shortcut icon" href="/ui/image/{$.teaFaviconFileId}" />
|
||||
{$end}
|
||||
<link rel="stylesheet" type="text/css" href="/_/@default/@layout.css" media="all"/>
|
||||
<link rel="stylesheet" type="text/css" href="/_/@default/@layout.css" media="all" />
|
||||
{$TEA.SEMANTIC}
|
||||
|
||||
{$TEA.VUE}
|
||||
@@ -20,7 +21,7 @@
|
||||
window.BRAND_DOCS_SITE = {$ jsonEncode .brandConfig.docsSite};
|
||||
window.BRAND_DOCS_PREFIX = {$ jsonEncode .brandConfig.docsPathPrefix};
|
||||
window.BRAND_PRODUCT_NAME = {$ jsonEncode .brandConfig.productName};
|
||||
|
||||
|
||||
// 确保 teaName 和 teaVersion 在 Vue 初始化前可用
|
||||
if (typeof window.TEA === "undefined") {
|
||||
window.TEA = {};
|
||||
@@ -36,122 +37,152 @@
|
||||
</script>
|
||||
<script type="text/javascript" src="/js/config/brand.js"></script>
|
||||
<script type="text/javascript" src="/_/@default/@layout.js"></script>
|
||||
<script type="text/javascript" src="/js/components.js"></script>
|
||||
<script type="text/javascript" src="/js/utils.min.js"></script>
|
||||
<script type="text/javascript" src="/js/sweetalert2/dist/sweetalert2.all.min.js" async></script>
|
||||
<script type="text/javascript" src="/js/date.tea.js"></script>
|
||||
<script type="text/javascript" src="/js/components.js"></script>
|
||||
<script type="text/javascript" src="/js/utils.min.js"></script>
|
||||
<script type="text/javascript" src="/js/sweetalert2/dist/sweetalert2.all.min.js" async></script>
|
||||
<script type="text/javascript" src="/js/date.tea.js"></script>
|
||||
<script type="text/javascript" src="/js/langs/base.js?v={$ .teaVersion}"></script>
|
||||
<script type="text/javascript" src="/js/langs/{$.teaLang}.js?v={$ .teaVersion}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/js/langs/{$.teaLang}.css?v={$ .teaVersion}"/>
|
||||
<link rel="stylesheet" type="text/css" href="/js/langs/{$.teaLang}.css?v={$ .teaVersion}" />
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/_/@default/@layout_override.css" media="all"/>
|
||||
<link rel="stylesheet" type="text/css" href="/_/@default/@layout_override.css" media="all" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div>
|
||||
<!-- 顶部导航 -->
|
||||
<div class="ui menu top-nav blue inverted small borderless" :class="(teaTheme == null || teaTheme.length == 0) ? 'theme2': teaTheme" v-cloak="">
|
||||
<a href="/" class="item">
|
||||
<i class="ui icon leaf" v-if="teaLogoFileId == 0"></i><img alt="logo" v-if="teaLogoFileId > 0" :src="'/ui/image/' + teaLogoFileId" style="width: auto;height: 1.6em"/> {{teaTitle}} <sup v-if="teaShowVersion">v{{teaVersion}}<span v-if="teaVersion.split('.').length == 4" title="当前版本为测试版,再次感谢您参与测试"> beta</span></sup>
|
||||
</a>
|
||||
|
||||
<div class="right menu">
|
||||
<!-- 集群同步 -->
|
||||
<a href="" class="item" v-if="teaCheckNodeTasks && doingNodeTasks.isUpdated" @click.prevent="showNodeTasks()">
|
||||
<span v-if="!doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span"><i class="icon cloud disabled"></i><span class="disabled">已同步节点</span></span>
|
||||
<span v-if="doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span rotate"><i class="icon cloud"></i><span>正在同步节点...</span></span>
|
||||
<span v-if="doingNodeTasks.hasError" class="red"><i class="icon cloud"></i>节点同步失败</span>
|
||||
<div>
|
||||
<!-- 顶部导航 -->
|
||||
<div class="ui menu top-nav blue inverted small borderless"
|
||||
:class="(teaTheme == null || teaTheme.length == 0) ? 'theme2': teaTheme" v-cloak="">
|
||||
<a href="/" class="item">
|
||||
<i class="ui icon leaf" v-if="teaLogoFileId == 0"></i><img alt="logo" v-if="teaLogoFileId > 0"
|
||||
:src="'/ui/image/' + teaLogoFileId" style="width: auto;height: 1.6em" />
|
||||
{{teaTitle}} <sup v-if="teaShowVersion">v{{teaVersion}}<span
|
||||
v-if="teaVersion.split('.').length == 4" title="当前版本为测试版,再次感谢您参与测试"> beta</span></sup>
|
||||
</a>
|
||||
|
||||
<!-- DNS同步 -->
|
||||
<a href="" class="item" v-if="teaCheckDNSTasks && doingDNSTasks.isUpdated" @click.prevent="showDNSTasks()">
|
||||
<span v-if="!doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span"><i class="icon globe disabled"></i><span class="disabled">已同步DNS</span></span>
|
||||
<span v-if="doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span rotate"><i class="icon globe"></i><span>正在同步DNS...</span></span>
|
||||
<span v-if="doingDNSTasks.hasError" class="red"><i class="icon globe"></i>DNS同步失败</span>
|
||||
</a>
|
||||
<div class="right menu">
|
||||
<!-- 集群同步 -->
|
||||
<a href="" class="item" v-if="teaCheckNodeTasks && doingNodeTasks.isUpdated"
|
||||
@click.prevent="showNodeTasks()">
|
||||
<span v-if="!doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span"><i
|
||||
class="icon cloud disabled"></i><span class="disabled">已同步节点</span></span>
|
||||
<span v-if="doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span rotate"><i
|
||||
class="icon cloud"></i><span>正在同步节点...</span></span>
|
||||
<span v-if="doingNodeTasks.hasError" class="red"><i class="icon cloud"></i>节点同步失败</span>
|
||||
</a>
|
||||
|
||||
<!-- 消息 -->
|
||||
<a href="" class="item" :class="{active:teaMenu == 'message'}" @click.prevent="showMessages()">
|
||||
<span v-if="globalMessageBadge > 0" class="blink hover-span"><i class="icon bell"></i><span>消息({{globalMessageBadge}}) </span></span>
|
||||
<span v-if="globalMessageBadge == 0" class="hover-span"><i class="icon bell disabled"></i><span class="disabled">消息(0)</span></span>
|
||||
</a>
|
||||
<!-- DNS同步 -->
|
||||
<a href="" class="item" v-if="teaCheckDNSTasks && doingDNSTasks.isUpdated"
|
||||
@click.prevent="showDNSTasks()">
|
||||
<span v-if="!doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span"><i
|
||||
class="icon globe disabled"></i><span class="disabled">已同步DNS</span></span>
|
||||
<span v-if="doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span rotate"><i
|
||||
class="icon globe"></i><span>正在同步DNS...</span></span>
|
||||
<span v-if="doingDNSTasks.hasError" class="red"><i class="icon globe"></i>DNS同步失败</span>
|
||||
</a>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<a href="/settings/profile" class="item">
|
||||
<i class="icon user" v-if="teaUserAvatar.length == 0"></i>
|
||||
<img class="avatar" alt="" :src="teaUserAvatar" v-if="teaUserAvatar.length > 0"/>
|
||||
<span class="hover-span"><span class="disabled">{{teaUsername}}</span></span>
|
||||
</a>
|
||||
<!-- 消息 -->
|
||||
<a href="" class="item" :class="{active:teaMenu == 'message'}" @click.prevent="showMessages()">
|
||||
<span v-if="globalMessageBadge > 0" class="blink hover-span"><i
|
||||
class="icon bell"></i><span>消息({{globalMessageBadge}}) </span></span>
|
||||
<span v-if="globalMessageBadge == 0" class="hover-span"><i class="icon bell disabled"></i><span
|
||||
class="disabled">消息(0)</span></span>
|
||||
</a>
|
||||
|
||||
<a href="" class="item" title="switch language" @click.prevent="switchLang" v-show="false"><i class="icon language"></i> </a>
|
||||
<!-- 用户信息 -->
|
||||
<a href="/settings/profile" class="item">
|
||||
<i class="icon user" v-if="teaUserAvatar.length == 0"></i>
|
||||
<img class="avatar" alt="" :src="teaUserAvatar" v-if="teaUserAvatar.length > 0" />
|
||||
<span class="hover-span"><span class="disabled">{{teaUsername}}</span></span>
|
||||
</a>
|
||||
|
||||
<!-- 背景颜色 -->
|
||||
<a href="" class="item" title="点击切换界面风格" @click.prevent="changeTheme()" v-if="false"><i class="icon adjust"></i></a>
|
||||
<a href="" class="item" title="switch language" @click.prevent="switchLang" v-show="false"><i
|
||||
class="icon language"></i> </a>
|
||||
|
||||
<!-- 企业版 -->
|
||||
<!-- <a :href="'/settings/authority'" class="item" title="商业版" :v-if="teaIsPlus"><i class="icon gem outline yellow"></i></a>-->
|
||||
<!-- 背景颜色 -->
|
||||
<a href="" class="item" title="点击切换界面风格" @click.prevent="changeTheme()" v-if="false"><i
|
||||
class="icon adjust"></i></a>
|
||||
|
||||
<!-- 退出登录 -->
|
||||
<a :href="Tea.url('logout')" class="item" title="安全退出登录"><i class="icon sign out"></i>
|
||||
<span class="hover-span"><span class="disabled">退出登录</span></span>
|
||||
</a>
|
||||
<!-- 企业版 -->
|
||||
<!-- <a :href="'/settings/authority'" class="item" title="商业版" :v-if="teaIsPlus"><i class="icon gem outline yellow"></i></a>-->
|
||||
|
||||
<!-- 退出登录 -->
|
||||
<a :href="Tea.url('logout')" class="item" title="安全退出登录"><i class="icon sign out"></i>
|
||||
<span class="hover-span"><span class="disabled">退出登录</span></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 左侧主菜单 -->
|
||||
<div class="main-menu" :class="(teaTheme == null || teaTheme.length == 0) ? 'theme2': teaTheme" v-cloak="">
|
||||
<div class="ui labeled menu vertical blue inverted tiny borderless">
|
||||
<div class="item"></div>
|
||||
<!--<a :href="Tea.url('dashboard')" class="item" :class="{active:teaMenu == 'dashboard'}">
|
||||
<!-- 左侧主菜单 -->
|
||||
<div class="main-menu" :class="(teaTheme == null || teaTheme.length == 0) ? 'theme2': teaTheme" v-cloak="">
|
||||
<div class="ui labeled menu vertical blue inverted tiny borderless">
|
||||
<div class="item"></div>
|
||||
<!--<a :href="Tea.url('dashboard')" class="item" :class="{active:teaMenu == 'dashboard'}">
|
||||
<i class="ui dashboard icon"></i>
|
||||
<span>仪表板</span>
|
||||
</a>-->
|
||||
|
||||
<!-- 模块 -->
|
||||
<div v-for="module in teaModules">
|
||||
<a class="item" :href="Tea.url(module.code)" :class="{active:teaMenu == module.code && teaSubMenu.length == 0, separator:module.code.length == 0, expend: teaMenu == module.code}" :style="(teaMenu == module.code && teaSubMenu.length == 0) ? 'background: rgba(230, 230, 230, 0.45) !important;' : ''" v-if="module.isOn !== false">
|
||||
<span v-if="module.code.length > 0">
|
||||
<i class="window restore outline icon" v-if="module.icon == null"></i>
|
||||
<i class="ui icon" v-if="module.icon != null" :class="module.icon"></i>
|
||||
<span class="module-name">{{module.name}}</span>
|
||||
</span>
|
||||
<div class="subtitle" v-if="module.subtitle != null && module.subtitle.length > 0">{{module.subtitle}}</div>
|
||||
</a>
|
||||
<div v-if="teaMenu == module.code" class="sub-items">
|
||||
<a class="item" :class="{separator:subItem.name == '-', active: subItem.code == teaSubMenu}" v-for="subItem in module.subItems" v-if="subItem.isOn !== false" :href="subItem.url" :style="(subItem.code == teaSubMenu) ? 'background: rgba(230, 230, 230, 0.55) !important;' : ''"><i class="icon angle right" v-if="subItem.name != '-' && subItem.code == teaSubMenu"></i> <span v-if="subItem.name != '-'">{{subItem.name}}</span>
|
||||
<span class="ui label tiny red" v-if="subItem.badge != null && subItem.badge > 0">
|
||||
<span v-if="subItem.badge < 100">{{subItem.badge}}</span>
|
||||
<span v-else>99+</span>
|
||||
<!-- 模块 -->
|
||||
<div v-for="module in teaModules">
|
||||
<a class="item" :href="Tea.url(module.code)"
|
||||
:class="{active:teaMenu == module.code && teaSubMenu.length == 0, separator:module.code.length == 0, expend: teaMenu == module.code}"
|
||||
:style="(teaMenu == module.code && teaSubMenu.length == 0) ? 'background: rgba(230, 230, 230, 0.45) !important;' : ''"
|
||||
v-if="module.isOn !== false">
|
||||
<span v-if="module.code.length > 0">
|
||||
<i class="window restore outline icon" v-if="module.icon == null"></i>
|
||||
<i class="ui icon" v-if="module.icon != null" :class="module.icon"></i>
|
||||
<span class="module-name">{{module.name}}</span>
|
||||
</span>
|
||||
<div class="subtitle" v-if="module.subtitle != null && module.subtitle.length > 0">
|
||||
{{module.subtitle}}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="teaMenu == module.code" class="sub-items">
|
||||
<a class="item" :class="{separator:subItem.name == '-', active: subItem.code == teaSubMenu}"
|
||||
v-for="subItem in module.subItems" v-if="subItem.isOn !== false" :href="subItem.url"
|
||||
:style="(subItem.code == teaSubMenu) ? 'background: rgba(230, 230, 230, 0.55) !important;' : ''"><i
|
||||
class="icon angle right" v-if="subItem.name != '-' && subItem.code == teaSubMenu"></i>
|
||||
<span v-if="subItem.name != '-'">{{subItem.name}}</span>
|
||||
<span class="ui label tiny red" v-if="subItem.badge != null && subItem.badge > 0">
|
||||
<span v-if="subItem.badge < 100">{{subItem.badge}}</span>
|
||||
<span v-else>99+</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧主操作栏 -->
|
||||
<div class="main"
|
||||
:class="{'without-menu':teaSubMenus.menus == null || teaSubMenus.menus.length == 0 || (teaSubMenus.menus.length == 1 && teaSubMenus.menus[0].alwaysActive), 'without-secondary-menu':teaSubMenus.alwaysMenu == null || teaSubMenus.alwaysMenu.items.length <= 1, 'without-footer':!teaShowOpenSourceInfo}"
|
||||
v-cloak="">
|
||||
<!-- 操作菜单 -->
|
||||
<div class="ui top menu tabular tab-menu small" v-if="teaTabbar.length > 1">
|
||||
<a class="item" v-for="item in teaTabbar"
|
||||
:class="{'active':item.isActive && !item.isDisabled, right:item.isRight, title: item.isTitle, icon: item.icon != null && item.icon.length > 0, disabled: item.isDisabled}"
|
||||
:href="item.url">
|
||||
<var>{{item.name}}<span v-if="item.subName.length > 0">({{item.subName}})</span><i
|
||||
class="icon small" :class="item.icon" v-if="item.icon != null && item.icon.length > 0"></i>
|
||||
</var>
|
||||
<var v-if="item.isTitle && typeof _data.node == 'object'">{{node.name}}</var>
|
||||
<div class="bottom-indicator" v-if="item.isActive && !item.isTitle"></div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 功能区 -->
|
||||
<div class="main-box">
|
||||
{$TEA.VIEW}
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部 -->
|
||||
{$template "/footer"}
|
||||
</div>
|
||||
|
||||
<!-- 右侧主操作栏 -->
|
||||
<div class="main" :class="{'without-menu':teaSubMenus.menus == null || teaSubMenus.menus.length == 0 || (teaSubMenus.menus.length == 1 && teaSubMenus.menus[0].alwaysActive), 'without-secondary-menu':teaSubMenus.alwaysMenu == null || teaSubMenus.alwaysMenu.items.length <= 1, 'without-footer':!teaShowOpenSourceInfo}" v-cloak="">
|
||||
<!-- 操作菜单 -->
|
||||
<div class="ui top menu tabular tab-menu small" v-if="teaTabbar.length > 1">
|
||||
<a class="item" v-for="item in teaTabbar" :class="{'active':item.isActive && !item.isDisabled, right:item.isRight, title: item.isTitle, icon: item.icon != null && item.icon.length > 0, disabled: item.isDisabled}" :href="item.url">
|
||||
<var>{{item.name}}<span v-if="item.subName.length > 0">({{item.subName}})</span><i class="icon small" :class="item.icon" v-if="item.icon != null && item.icon.length > 0"></i> </var>
|
||||
<var v-if="item.isTitle && typeof _data.node == 'object'">{{node.name}}</var>
|
||||
<div class="bottom-indicator" v-if="item.isActive && !item.isTitle"></div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 功能区 -->
|
||||
<div class="main-box">
|
||||
{$TEA.VIEW}
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部 -->
|
||||
{$template "/footer"}
|
||||
</div>
|
||||
|
||||
{$echo "footer"}
|
||||
{$echo "footer"}
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -3,60 +3,76 @@
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
a.disabled,
|
||||
a.disabled:hover,
|
||||
a.disabled:active,
|
||||
span.disabled {
|
||||
color: #ccc !important;
|
||||
}
|
||||
|
||||
a.enabled,
|
||||
span.enabled,
|
||||
span.green {
|
||||
color: #21ba45;
|
||||
}
|
||||
|
||||
span.grey,
|
||||
label.grey,
|
||||
p.grey {
|
||||
color: grey !important;
|
||||
}
|
||||
|
||||
p.grey {
|
||||
margin-top: 0.8em;
|
||||
}
|
||||
|
||||
span.red,
|
||||
pre.red {
|
||||
color: #db2828;
|
||||
}
|
||||
|
||||
span.blue {
|
||||
color: #4183c4;
|
||||
}
|
||||
|
||||
pre:not(.CodeMirror-line) {
|
||||
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif !important;
|
||||
}
|
||||
|
||||
tbody {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.table.width30 {
|
||||
width: 30em !important;
|
||||
}
|
||||
|
||||
.table.width35 {
|
||||
width: 35em !important;
|
||||
}
|
||||
|
||||
.table.width40 {
|
||||
width: 40em !important;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
font-size: 0.9em !important;
|
||||
}
|
||||
|
||||
p.comment,
|
||||
div.comment {
|
||||
color: #959da6;
|
||||
@@ -65,37 +81,46 @@ div.comment {
|
||||
word-break: break-all;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
p.comment em,
|
||||
div.comment em {
|
||||
font-style: italic !important;
|
||||
}
|
||||
|
||||
.truncate {
|
||||
white-space: nowrap;
|
||||
-ms-text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
div.margin,
|
||||
p.margin {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
/** 操作按钮容器 **/
|
||||
.op.one {
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
.op.two {
|
||||
width: 7.4em;
|
||||
}
|
||||
|
||||
.op.three {
|
||||
width: 9em;
|
||||
}
|
||||
|
||||
.op.four {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
/** 扩展UI **/
|
||||
.field.text {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
/** 右侧主操作区 **/
|
||||
.main {
|
||||
position: absolute;
|
||||
@@ -105,166 +130,210 @@ p.margin {
|
||||
padding-right: 1em;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 512px) {
|
||||
.main {
|
||||
left: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.main.without-menu {
|
||||
left: 9em;
|
||||
}
|
||||
|
||||
.main.without-secondary-menu {
|
||||
top: 2.9em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 512px) {
|
||||
.main.without-menu {
|
||||
left: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.main table td.title {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.main table td.middle-title {
|
||||
width: 14em;
|
||||
}
|
||||
|
||||
.main table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table td.color-border {
|
||||
border-left: 1px #276ac6 solid !important;
|
||||
}
|
||||
|
||||
.main table td.vertical-top {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.main table td.vertical-middle {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.main table td[colspan="2"] a {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.main table td em {
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.main h3 {
|
||||
font-weight: normal;
|
||||
margin-top: 0.5em !important;
|
||||
}
|
||||
|
||||
.main h3 span {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.main h3 span.label {
|
||||
color: #6435c9;
|
||||
}
|
||||
|
||||
.main h3 a {
|
||||
margin-left: 1em;
|
||||
font-size: 14px !important;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
.main h3 a::before {
|
||||
content: "[";
|
||||
}
|
||||
|
||||
.main h3 a::after {
|
||||
content: "]";
|
||||
}
|
||||
|
||||
.main h4 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.main td span.small {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.main .button.mini {
|
||||
font-size: 0.8em;
|
||||
padding: 0.2em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
/** 右侧文本子菜单 **/
|
||||
.text.menu {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.text.menu::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
/** Vue **/
|
||||
[v-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/** auto complete **/
|
||||
.autocomplete-box .menu {
|
||||
background: #eee !important;
|
||||
}
|
||||
|
||||
.autocomplete-box .menu::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.autocomplete-box .menu .item {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
select.auto-width {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
/** column **/
|
||||
@media screen and (max-width: 512px) {
|
||||
.column:not(.one) {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/** label **/
|
||||
label[for] {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
label.blue {
|
||||
color: #2185d0 !important;
|
||||
}
|
||||
|
||||
/** Menu **/
|
||||
.first-menu .menu.text {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.first-menu .divider {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.second-menu .menu.text {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.second-menu .menu.text em {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.second-menu .divider {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.menu a {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/** var **/
|
||||
span.olive,
|
||||
var.olive {
|
||||
color: #b5cc18 !important;
|
||||
}
|
||||
|
||||
span.dash {
|
||||
border-bottom: 1px dashed grey;
|
||||
}
|
||||
|
||||
span.hover:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
/** Message **/
|
||||
.message .gopher {
|
||||
width: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/** checkbox **/
|
||||
.checkbox label a,
|
||||
.checkbox label {
|
||||
font-size: 0.8em !important;
|
||||
}
|
||||
|
||||
/** page **/
|
||||
.page {
|
||||
margin-top: 1em;
|
||||
border-left: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.page a {
|
||||
display: inline-block;
|
||||
background: #fafafa;
|
||||
@@ -275,40 +344,50 @@ span.hover:hover {
|
||||
border: 1px solid #ddd;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.page a.active {
|
||||
background: #2185d0 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page a:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.page select {
|
||||
padding-top: 0.3em !important;
|
||||
padding-bottom: 0.3em !important;
|
||||
}
|
||||
|
||||
.swal2-html-container {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.swal2-confirm:focus,
|
||||
.swal2-cancel:focus,
|
||||
.swal2-close:focus {
|
||||
border: 3px #ddd solid !important;
|
||||
}
|
||||
|
||||
.swal2-confirm,
|
||||
.swal2-cancel,
|
||||
.swal2-close {
|
||||
border: 3px #fff solid !important;
|
||||
}
|
||||
|
||||
.swal2-cancel {
|
||||
margin-left: 2em !important;
|
||||
}
|
||||
|
||||
input.error {
|
||||
border: 1px #e0b4b4 solid !important;
|
||||
}
|
||||
|
||||
textarea.wide-code {
|
||||
font-family: Menlo, Monaco, "Courier New", monospace !important;
|
||||
line-height: 1.6 !important;
|
||||
}
|
||||
|
||||
.combo-box .menu {
|
||||
max-height: 17em;
|
||||
overflow-y: auto;
|
||||
@@ -317,9 +396,11 @@ textarea.wide-code {
|
||||
border-top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.combo-box .menu::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
code-label {
|
||||
background: #fff;
|
||||
border: 1px solid rgba(34, 36, 38, 0.15);
|
||||
@@ -333,4 +414,62 @@ code-label {
|
||||
font-weight: 700;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/*# sourceMappingURL=@layout_popup.css.map */
|
||||
|
||||
/*# sourceMappingURL=@layout_popup.css.map */
|
||||
/* Override Primary Button Color for WAF Platform */
|
||||
.ui.primary.button,
|
||||
.ui.primary.buttons .button {
|
||||
background-color: #0f2c54 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.ui.primary.button:hover,
|
||||
.ui.primary.buttons .button:hover {
|
||||
background-color: #0a1f3a !important;
|
||||
}
|
||||
|
||||
.ui.primary.button:focus,
|
||||
.ui.primary.buttons .button:focus {
|
||||
background-color: #08192e !important;
|
||||
}
|
||||
|
||||
.ui.primary.button:active,
|
||||
.ui.primary.buttons .button:active,
|
||||
.ui.primary.active.button {
|
||||
background-color: #050d18 !important;
|
||||
}
|
||||
|
||||
.text-primary,
|
||||
.blue {
|
||||
color: #0f2c54 !important;
|
||||
}
|
||||
|
||||
/* Override Semantic UI Default Blue */
|
||||
.ui.blue.button,
|
||||
.ui.blue.buttons .button {
|
||||
background-color: #0f2c54 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.ui.blue.button:hover,
|
||||
.ui.blue.buttons .button:hover {
|
||||
background-color: #0a1f3a !important;
|
||||
}
|
||||
|
||||
.ui.basic.blue.button,
|
||||
.ui.basic.blue.buttons .button {
|
||||
box-shadow: 0 0 0 1px #0f2c54 inset !important;
|
||||
color: #0f2c54 !important;
|
||||
}
|
||||
|
||||
.ui.basic.blue.button:hover,
|
||||
.ui.basic.blue.buttons .button:hover {
|
||||
background: transparent !important;
|
||||
box-shadow: 0 0 0 1px #0a1f3a inset !important;
|
||||
color: #0a1f3a !important;
|
||||
}
|
||||
|
||||
.ui.menu .active.item {
|
||||
border-color: #2185d0 !important;
|
||||
color: #2185d0 !important;
|
||||
}
|
||||
@@ -8,16 +8,10 @@
|
||||
<div class="item"><strong>上传 SDK</strong></div>
|
||||
</second-menu>
|
||||
|
||||
<form method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="ui form"
|
||||
data-tea-action="$"
|
||||
data-tea-timeout="300"
|
||||
data-tea-before="beforeUpload"
|
||||
data-tea-done="doneUpload"
|
||||
data-tea-success="successUpload">
|
||||
<form method="post" enctype="multipart/form-data" class="ui form" data-tea-action="$" data-tea-timeout="300"
|
||||
data-tea-before="beforeUpload" data-tea-done="doneUpload" data-tea-success="successUpload">
|
||||
<csrf-token></csrf-token>
|
||||
<input type="hidden" name="appId" :value="app.id"/>
|
||||
<input type="hidden" name="appId" :value="app.id" />
|
||||
|
||||
<table class="ui table selectable definition">
|
||||
<tr>
|
||||
@@ -33,22 +27,22 @@
|
||||
<tr>
|
||||
<td class="title">版本号 *</td>
|
||||
<td>
|
||||
<input type="text" name="version" v-model="version" maxlength="32"/>
|
||||
<input type="text" name="version" v-model="version" maxlength="32" />
|
||||
<p class="comment">默认 `1.0.0`。同平台+同版本再次上传会覆盖该版本文件。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">SDK 包</td>
|
||||
<td>
|
||||
<input type="file" name="sdkFile" accept=".zip"/>
|
||||
<p class="comment">支持 zip 包,例如 `httpdns-sdk-android.zip`。</p>
|
||||
<input type="file" name="sdkFile" accept=".zip" />
|
||||
<p class="comment">支持 zip 包,例如 `httpdns-sdk-android.zip`,单文件最大 20MB。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">集成文档</td>
|
||||
<td>
|
||||
<input type="file" name="docFile" accept=".md,text/markdown"/>
|
||||
<p class="comment">支持 Markdown 文件(`.md`)。</p>
|
||||
<input type="file" name="docFile" accept=".md,text/markdown" />
|
||||
<p class="comment">支持 Markdown 文件(`.md`),单文件最大 20MB。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -65,26 +59,26 @@
|
||||
<h4 style="margin-top: 1.5em">已上传文件</h4>
|
||||
<table class="ui table selectable celled" v-if="uploadedFiles.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>平台</th>
|
||||
<th>类型</th>
|
||||
<th>版本</th>
|
||||
<th>文件名</th>
|
||||
<th>大小</th>
|
||||
<th>更新时间</th>
|
||||
<th class="one wide">操作</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>平台</th>
|
||||
<th>类型</th>
|
||||
<th>版本</th>
|
||||
<th>文件名</th>
|
||||
<th>大小</th>
|
||||
<th>更新时间</th>
|
||||
<th class="one wide">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="file in uploadedFiles">
|
||||
<td>{{file.platform}}</td>
|
||||
<td>{{file.fileType}}</td>
|
||||
<td>{{file.version}}</td>
|
||||
<td>{{file.name}}</td>
|
||||
<td>{{file.sizeText}}</td>
|
||||
<td>{{file.updatedAt}}</td>
|
||||
<td><a href="" @click.prevent="deleteUploadedFile(file.name)">删除</a></td>
|
||||
</tr>
|
||||
<tr v-for="file in uploadedFiles">
|
||||
<td>{{file.platform}}</td>
|
||||
<td>{{file.fileType}}</td>
|
||||
<td>{{file.version}}</td>
|
||||
<td>{{file.name}}</td>
|
||||
<td>{{file.sizeText}}</td>
|
||||
<td>{{file.updatedAt}}</td>
|
||||
<td><a href="" @click.prevent="deleteUploadedFile(file.name)">删除</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="ui message" v-else>
|
||||
|
||||
@@ -8,6 +8,17 @@
|
||||
}
|
||||
|
||||
this.beforeUpload = function () {
|
||||
var maxSize = 20 * 1024 * 1024; // 20MB
|
||||
var inputs = document.querySelectorAll("input[type=file]");
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
var files = inputs[i].files;
|
||||
if (files != null && files.length > 0) {
|
||||
if (files[0].size > maxSize) {
|
||||
teaweb.warn("文件 \"" + files[0].name + "\" 超过 20MB 限制(" + (files[0].size / 1024 / 1024).toFixed(1) + "MB),请压缩后重试");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.isUploading = true;
|
||||
};
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
<td>
|
||||
v{{node.status.buildVersion}}
|
||||
|
||||
<a :href="'/httpdns/clusters/upgradeRemote?clusterId=' + clusterId + '&nodeId=' + node.id" v-if="shouldUpgrade"><span class="red">发现新版本 v{{newVersion}} »</span></a>
|
||||
<a :href="'/httpdns/clusters/cluster/upgradeRemote?clusterId=' + clusterId" v-if="shouldUpgrade"><span class="red">发现新版本 v{{newVersion}} »</span></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="node.status.exePath != null && node.status.exePath.length > 0">
|
||||
|
||||
@@ -66,30 +66,37 @@
|
||||
<tr>
|
||||
<td>自动远程启动</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="autoRemoteStart" value="1" v-model="settings.autoRemoteStart" />
|
||||
<label></label>
|
||||
</div>
|
||||
<checkbox name="autoRemoteStart" v-model="settings.autoRemoteStart"></checkbox>
|
||||
<p class="comment">当检测到节点离线时,自动尝试远程启动(前提是节点已经设置了SSH登录认证)。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>访问日志</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="accessLogIsOn" value="1" v-model="settings.accessLogIsOn" />
|
||||
<label></label>
|
||||
</div>
|
||||
<checkbox name="accessLogIsOn" v-model="settings.accessLogIsOn"></checkbox>
|
||||
<p class="comment">启用后,HTTPDNS 节点将会记录客户端的请求访问日志。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>时区</td>
|
||||
<td>
|
||||
<div>
|
||||
<span class="ui label basic small" v-if="timeZoneLocation != null">当前:{{timeZoneLocation.name}}
|
||||
({{timeZoneLocation.offset}})</span>
|
||||
</div>
|
||||
<div style="margin-top: 0.5em">
|
||||
<select class="ui dropdown auto-width" name="timeZone" v-model="settings.timeZone">
|
||||
<option v-for="tz in timeZoneLocations" :value="tz.name">{{tz.name}} ({{tz.offset}})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="comment">HTTPDNS 节点进程使用的时区设置,默认为 Asia/Shanghai。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>启用当前集群</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="isOn" value="1" v-model="settings.isOn" />
|
||||
<label></label>
|
||||
</div>
|
||||
<checkbox name="isOn" v-model="settings.isOn"></checkbox>
|
||||
<p class="comment">取消启用后,该集群不会参与 HTTPDNS 服务。</p>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -110,4 +117,4 @@
|
||||
|
||||
<submit-btn></submit-btn>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,4 +7,27 @@ Tea.context(function () {
|
||||
if (!this.settings) {
|
||||
this.settings = {}
|
||||
}
|
||||
|
||||
let toBool = function (v) {
|
||||
if (typeof v === "boolean") {
|
||||
return v
|
||||
}
|
||||
if (typeof v === "number") {
|
||||
return v === 1
|
||||
}
|
||||
if (typeof v === "string") {
|
||||
let s = v.toLowerCase().trim()
|
||||
return s === "1" || s === "true" || s === "on" || s === "yes" || s === "enabled"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
this.settings.autoRemoteStart = toBool(this.settings.autoRemoteStart)
|
||||
this.settings.accessLogIsOn = toBool(this.settings.accessLogIsOn)
|
||||
this.settings.isOn = toBool(this.settings.isOn)
|
||||
|
||||
if (!this.settings.timeZone || this.settings.timeZone.length == 0) {
|
||||
this.settings.timeZone = "Asia/Shanghai"
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<tr>
|
||||
<td>节点安装根目录</td>
|
||||
<td>
|
||||
<input type="text" name="installDir" maxlength="255" value="/opt/edge-httpdns" />
|
||||
<input type="text" name="installDir" maxlength="255" value="/root/edge-httpdns" />
|
||||
<p class="comment">边缘节点安装 HTTPDNS 服务的默认目录。</p>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -59,4 +59,4 @@
|
||||
</tr>
|
||||
</table>
|
||||
<submit-btn></submit-btn>
|
||||
</form>
|
||||
</form>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<tr>
|
||||
<td>安装目录</td>
|
||||
<td>
|
||||
<input type="text" name="installDir" maxlength="100" :value="cluster.installDir || '/opt/edge-httpdns'" />
|
||||
<input type="text" name="installDir" maxlength="100" :value="cluster.installDir || '/root/edge-httpdns'" />
|
||||
<p class="comment">默认使用集群配置目录。</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -51,6 +51,12 @@
|
||||
:class="{red:cluster.countAllNodes > cluster.countActiveNodes}">{{cluster.countAllNodes}}</span>
|
||||
</a>
|
||||
<span class="disabled" v-else="">-</span>
|
||||
|
||||
<div v-if="cluster.countUpgradeNodes > 0" style="margin-top:0.5em">
|
||||
<a :href="'/httpdns/clusters/cluster/upgradeRemote?clusterId=' + cluster.id" title="点击进入远程升级页面">
|
||||
<span class="red">有{{cluster.countUpgradeNodes}}个节点需要升级</span>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="center">
|
||||
<a :href="'/httpdns/clusters/cluster?clusterId=' + cluster.id" v-if="cluster.countActiveNodes > 0">
|
||||
@@ -67,4 +73,4 @@
|
||||
</table>
|
||||
|
||||
<div class="page" v-html="page"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,62 @@
|
||||
{$layout}
|
||||
{$layout}
|
||||
|
||||
<p class="ui message error">姝ゅ姛鑳芥殏鏈紑鏀俱€?/p>
|
||||
<p class="comment" v-if="nodes.length == 0">暂时没有需要升级的节点。</p>
|
||||
|
||||
<div v-if="nodes.length > 0">
|
||||
<h3>
|
||||
所有需要升级的节点
|
||||
<button class="ui button primary tiny" v-if="countCheckedNodes() > 0" @click.prevent="installBatch()">批量升级({{countCheckedNodes()}})</button>
|
||||
</h3>
|
||||
<table class="ui table selectable celled">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:3em">
|
||||
<checkbox @input="checkNodes"></checkbox>
|
||||
</th>
|
||||
<th>节点名</th>
|
||||
<th>访问IP</th>
|
||||
<th>SSH地址</th>
|
||||
<th>版本变化</th>
|
||||
<th class="four wide">节点状态</th>
|
||||
<th class="two op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="node in nodes">
|
||||
<td>
|
||||
<checkbox v-model="node.isChecked" v-if="node.installStatus == null || !node.installStatus.isOk"></checkbox>
|
||||
</td>
|
||||
<td>
|
||||
<a :href="'/httpdns/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + node.id">{{node.name}}</a>
|
||||
<a :href="'/httpdns/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + node.id" title="节点详情" style="margin-left: 0.4em">[详情]</a>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="node.accessIP.length > 0" class="ui label tiny basic">{{node.accessIP}}</span>
|
||||
<span v-else class="disabled">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="node.login != null && node.login.type == 'ssh' && node.loginParams != null && node.loginParams.host != null && node.loginParams.host.length > 0">
|
||||
{{node.loginParams.host}}:{{node.loginParams.port}}
|
||||
</span>
|
||||
<span v-else class="disabled">没有设置</span>
|
||||
</td>
|
||||
<td>v{{node.oldVersion}} -> v{{node.newVersion}}</td>
|
||||
<td>
|
||||
<div v-if="node.installStatus != null && (node.installStatus.isRunning || node.installStatus.isFinished)">
|
||||
<div v-if="node.installStatus.isRunning" class="blue">升级中...</div>
|
||||
<div v-if="node.installStatus.isFinished">
|
||||
<span v-if="node.installStatus.isOk" class="green">已升级成功</span>
|
||||
<span v-if="!node.installStatus.isOk" class="red">升级过程中发生错误:{{node.installStatus.error}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="disabled">等待升级</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="" @click.prevent="installNode(node)" v-if="!isInstalling">升级</a>
|
||||
<span v-if="isInstalling && node.isInstalling">升级中...</span>
|
||||
<span v-if="isInstalling && !node.isInstalling" class="disabled">升级</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
147
EdgeAdmin/web/views/@default/httpdns/clusters/upgradeRemote.js
Normal file
147
EdgeAdmin/web/views/@default/httpdns/clusters/upgradeRemote.js
Normal file
@@ -0,0 +1,147 @@
|
||||
Tea.context(function () {
|
||||
this.isInstalling = false
|
||||
this.isBatch = false
|
||||
let installingNode = null
|
||||
|
||||
this.nodes.forEach(function (v) {
|
||||
v.isChecked = false
|
||||
})
|
||||
|
||||
this.$delay(function () {
|
||||
this.reload()
|
||||
})
|
||||
|
||||
let that = this
|
||||
|
||||
this.checkNodes = function (isChecked) {
|
||||
this.nodes.forEach(function (v) {
|
||||
v.isChecked = isChecked
|
||||
})
|
||||
}
|
||||
|
||||
this.countCheckedNodes = function () {
|
||||
return that.nodes.$count(function (k, v) {
|
||||
return v.isChecked
|
||||
})
|
||||
}
|
||||
|
||||
this.installNode = function (node) {
|
||||
let that = this
|
||||
if (this.isBatch) {
|
||||
installingNode = node
|
||||
that.isInstalling = true
|
||||
node.isInstalling = true
|
||||
|
||||
that.$post("$")
|
||||
.params({
|
||||
nodeId: node.id
|
||||
})
|
||||
} else {
|
||||
teaweb.confirm("确定要开始升级此节点吗?", function () {
|
||||
installingNode = node
|
||||
that.isInstalling = true
|
||||
node.isInstalling = true
|
||||
|
||||
that.$post("$")
|
||||
.params({
|
||||
nodeId: node.id
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.installBatch = function () {
|
||||
let that = this
|
||||
this.isBatch = true
|
||||
teaweb.confirm("确定要批量升级选中的节点吗?", function () {
|
||||
that.installNext()
|
||||
})
|
||||
}
|
||||
|
||||
this.installNext = function () {
|
||||
let nextNode = this.nodes.$find(function (k, v) {
|
||||
return v.isChecked
|
||||
})
|
||||
|
||||
if (nextNode == null) {
|
||||
teaweb.success("全部升级成功", function () {
|
||||
teaweb.reload()
|
||||
})
|
||||
} else {
|
||||
this.installNode(nextNode)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.reload = function () {
|
||||
let that = this
|
||||
if (installingNode != null) {
|
||||
this.$post("/httpdns/clusters/upgradeStatus")
|
||||
.params({
|
||||
nodeId: installingNode.id
|
||||
})
|
||||
.success(function (resp) {
|
||||
if (resp.data.status != null) {
|
||||
installingNode.installStatus = resp.data.status
|
||||
if (installingNode.installStatus.isFinished) {
|
||||
if (installingNode.installStatus.isOk) {
|
||||
installingNode.isChecked = false
|
||||
installingNode = null
|
||||
if (that.isBatch) {
|
||||
that.installNext()
|
||||
} else {
|
||||
teaweb.success("升级成功", function () {
|
||||
teaweb.reload()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let nodeId = installingNode.id
|
||||
let errMsg = installingNode.installStatus.error
|
||||
that.isInstalling = false
|
||||
installingNode.isInstalling = false
|
||||
installingNode = null
|
||||
|
||||
switch (resp.data.status.errorCode) {
|
||||
case "EMPTY_LOGIN":
|
||||
case "EMPTY_SSH_HOST":
|
||||
case "EMPTY_SSH_PORT":
|
||||
case "EMPTY_GRANT":
|
||||
teaweb.warn("需要填写SSH登录信息", function () {
|
||||
teaweb.popup("/httpdns/clusters/updateNodeSSH?nodeId=" + nodeId, {
|
||||
height: "20em",
|
||||
callback: function () {
|
||||
teaweb.reload()
|
||||
}
|
||||
})
|
||||
})
|
||||
return
|
||||
case "CREATE_ROOT_DIRECTORY_FAILED":
|
||||
teaweb.warn("创建根目录失败,请检查目录权限或手工创建:" + errMsg)
|
||||
return
|
||||
case "INSTALL_HELPER_FAILED":
|
||||
teaweb.warn("安装助手失败:" + errMsg)
|
||||
return
|
||||
case "TEST_FAILED":
|
||||
teaweb.warn("环境测试失败:" + errMsg)
|
||||
return
|
||||
case "RPC_TEST_FAILED":
|
||||
teaweb.confirm("html:要升级的节点到API服务之间的RPC通讯测试失败,具体错误:" + errMsg + ",<br/>现在修改API信息?", function () {
|
||||
window.location = "/settings/api"
|
||||
})
|
||||
return
|
||||
default:
|
||||
teaweb.warn("升级失败:" + errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.done(function () {
|
||||
setTimeout(this.reload, 3000)
|
||||
})
|
||||
} else {
|
||||
setTimeout(this.reload, 3000)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</thead>
|
||||
<tr v-for="node in nodes">
|
||||
<td class="node-name-td"><a :href="'/ns/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + node.id"><keyword :v-word="keyword">{{node.name}}</keyword></a>
|
||||
<div v-if="node.status != null && node.status.version.length > 0 && node.status.version != latestVersion">
|
||||
<div v-if="node.status != null && node.status.version.length > 0 && latestVersion.length > 0 && Tea.versionCompare(latestVersion, node.status.version) > 0">
|
||||
<span class="small red">v{{node.status.version}} -> v{{latestVersion}}</span>
|
||||
</div>
|
||||
|
||||
@@ -112,4 +112,4 @@
|
||||
</table>
|
||||
|
||||
|
||||
<div class="page" v-html="page"></div>
|
||||
<div class="page" v-html="page"></div>
|
||||
|
||||
@@ -97,4 +97,4 @@
|
||||
</tr>
|
||||
</table>
|
||||
<p class="comment">数据更新于{{key.updatedTime}}。</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,45 +1,38 @@
|
||||
{$layout}
|
||||
|
||||
<form class="ui form" method="post" data-tea-action="$" data-tea-success="success">
|
||||
<csrf-token></csrf-token>
|
||||
<csrf-token></csrf-token>
|
||||
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">产品名称 *</td>
|
||||
<td>
|
||||
<input type="text" name="productName" v-model="config.productName" maxlength="100"/>
|
||||
<p class="comment">可以使用变量<code-label>${product.name}</code-label>在网页里展示此名称,建议仅包含英文和数字字符。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>管理员系统名称 *</td>
|
||||
<td>
|
||||
<input type="text" name="adminSystemName" v-model="config.adminSystemName" maxlength="100"/>
|
||||
<p class="comment">当前管理系统界面上显示的名称。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>显示版本号</td>
|
||||
<td>
|
||||
<checkbox name="showVersion" v-model="config.showVersion"></checkbox>
|
||||
<p class="comment">选中后,在界面中显示系统版本号。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>定制版本号</td>
|
||||
<td>
|
||||
<input type="text" name="version" v-model="config.version" maxlength="100"/>
|
||||
<p class="comment">定制自己的版本号,留空表示使用系统自带的版本号。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="teaIsPlus">
|
||||
<td>显示模块</td>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">产品名称 *</td>
|
||||
<td>
|
||||
<checkbox name="supportModuleCDN" v-model="supportModuleCDN">CDN</checkbox>
|
||||
<checkbox name="supportModuleNS" v-model="supportModuleNS" v-show="nsIsVisible">智能DNS</checkbox>
|
||||
<p class="comment">当前管理系统中可以显示的模块,不能为空。</p>
|
||||
<input type="text" name="productName" v-model="config.productName" maxlength="100" />
|
||||
<p class="comment">可以使用变量<code-label>${product.name}</code-label>在网页里展示此名称,建议仅包含英文和数字字符。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>管理员系统名称 *</td>
|
||||
<td>
|
||||
<input type="text" name="adminSystemName" v-model="config.adminSystemName" maxlength="100" />
|
||||
<p class="comment">当前管理系统界面上显示的名称。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>显示版本号</td>
|
||||
<td>
|
||||
<checkbox name="showVersion" v-model="config.showVersion"></checkbox>
|
||||
<p class="comment">选中后,在界面中显示系统版本号。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>定制版本号</td>
|
||||
<td>
|
||||
<input type="text" name="version" v-model="config.version" maxlength="100" />
|
||||
<p class="comment">定制自己的版本号,留空表示使用系统自带的版本号。</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>显示财务相关功能</td>
|
||||
<td>
|
||||
@@ -51,13 +44,14 @@
|
||||
<td>浏览器图标</td>
|
||||
<td>
|
||||
<div v-if="config.faviconFileId > 0">
|
||||
<a :href="'/ui/image/' + config.faviconFileId" target="_blank"><img alt="" :src="'/ui/image/' + config.faviconFileId" style="width:32px;border:1px #ccc solid;"/></a>
|
||||
<a :href="'/ui/image/' + config.faviconFileId" target="_blank"><img alt=""
|
||||
:src="'/ui/image/' + config.faviconFileId" style="width:32px;border:1px #ccc solid;" /></a>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="disabled">还没有上传。</span>
|
||||
</div>
|
||||
<div style="margin-top: 0.8em">
|
||||
<input type="file" name="faviconFile" accept=".png"/>
|
||||
<input type="file" name="faviconFile" accept=".png" />
|
||||
</div>
|
||||
<p class="comment">在浏览器标签栏显示的图标,请使用PNG格式。</p>
|
||||
</td>
|
||||
@@ -66,25 +60,27 @@
|
||||
<td>Logo</td>
|
||||
<td>
|
||||
<div v-if="config.logoFileId > 0">
|
||||
<a :href="'/ui/image/' + config.logoFileId" target="_blank"><img :src="'/ui/image/' + config.logoFileId" style="width:32px;border:1px #ccc solid;"/></a>
|
||||
<a :href="'/ui/image/' + config.logoFileId" target="_blank"><img
|
||||
:src="'/ui/image/' + config.logoFileId" style="width:32px;border:1px #ccc solid;" /></a>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="disabled">还没有上传。</span>
|
||||
</div>
|
||||
<div style="margin-top: 0.8em">
|
||||
<input type="file" name="logoFile" accept=".png"/>
|
||||
<input type="file" name="logoFile" accept=".png" />
|
||||
</div>
|
||||
<p class="comment">显示在系统界面上的图标,请使用PNG格式。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</table>
|
||||
|
||||
<h4>其他</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">每页显示数</td>
|
||||
<td>
|
||||
<input type="text" name="defaultPageSize" v-model="config.defaultPageSize" maxlength="3" style="width: 4em"/>
|
||||
<input type="text" name="defaultPageSize" v-model="config.defaultPageSize" maxlength="3"
|
||||
style="width: 4em" />
|
||||
<p class="comment">在有分页的地方每页显示数量;不能超过100。</p>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -94,12 +90,15 @@
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" v-model="timeZoneGroupCode">
|
||||
<option v-for="timeZoneGroup in timeZoneGroups" :value="timeZoneGroup.code">{{timeZoneGroup.name}}</option>
|
||||
<option v-for="timeZoneGroup in timeZoneGroups" :value="timeZoneGroup.code">
|
||||
{{timeZoneGroup.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" name="timeZone" v-model="config.timeZone">
|
||||
<option v-for="timeZoneLocation in timeZoneLocations" :value="timeZoneLocation.name" v-if="timeZoneLocation.group == timeZoneGroupCode">{{timeZoneLocation.name}} ({{timeZoneLocation.offset}})</option>
|
||||
<option v-for="timeZoneLocation in timeZoneLocations" :value="timeZoneLocation.name"
|
||||
v-if="timeZoneLocation.group == timeZoneGroupCode">{{timeZoneLocation.name}}
|
||||
({{timeZoneLocation.offset}})</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,5 +118,5 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<submit-btn></submit-btn>
|
||||
<submit-btn></submit-btn>
|
||||
</form>
|
||||
@@ -1,3 +1,139 @@
|
||||
{$layout}
|
||||
|
||||
<p class="comment">此功能暂未开放,敬请期待。</p>
|
||||
<div class="margin"></div>
|
||||
|
||||
<!-- 自动升级设置 -->
|
||||
<div class="ui segment">
|
||||
<h3>自动升级</h3>
|
||||
<table class="ui table definition">
|
||||
<tr>
|
||||
<td class="title">开启自动升级</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="autoUpgrade" v-model="config.autoUpgrade" @change="updateAutoUpgrade">
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">开启后,边缘节点、DNS节点、HTTPDNS节点每分钟检查新版本并自动下载升级。关闭后节点不会自动升级,但管理员仍可在下方手动升级。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="margin"></div>
|
||||
|
||||
<!-- 手动升级 -->
|
||||
<div class="ui segment">
|
||||
<h3 style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>手动升级</span>
|
||||
<button class="ui button primary tiny" v-if="totalUpgradeCount > 0"
|
||||
@click.prevent="upgradeAll()">全部升级({{totalUpgradeCount}})</button>
|
||||
</h3>
|
||||
|
||||
<div v-if="modules.length == 0">
|
||||
<p class="comment">暂无需要升级的节点。</p>
|
||||
</div>
|
||||
|
||||
<div v-for="mod in modules" class="ui segment" style="margin-bottom: 1em; padding: 0;">
|
||||
<h4 style="cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 0.8em 1em; margin: 0; background: #f9fafb; border-bottom: 1px solid rgba(34,36,38,.15);"
|
||||
@click.prevent="mod.expanded = !mod.expanded">
|
||||
<div>
|
||||
<i class="icon angle down" v-if="mod.expanded"></i>
|
||||
<i class="icon angle right" v-else></i>
|
||||
{{mod.name}}
|
||||
<span class="ui label tiny basic" v-if="mod.count > 0"
|
||||
style="margin-left: 0.5em;">{{mod.count}}个待升级</span>
|
||||
</div>
|
||||
<button class="ui button primary tiny" v-if="mod.count > 0"
|
||||
@click.stop.prevent="upgradeModule(mod.code)">升级所有{{mod.name}}</button>
|
||||
</h4>
|
||||
|
||||
<div v-show="mod.expanded" style="padding: 1em;">
|
||||
<div v-for="cluster in mod.clusters" style="margin-bottom: 1em;">
|
||||
<h5 style="cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 0.6em; background: #f3f4f5; border-radius: 4px; margin: 0;"
|
||||
@click.prevent="cluster.expanded = !cluster.expanded">
|
||||
<div>
|
||||
<i class="icon angle down" v-if="cluster.expanded"></i>
|
||||
<i class="icon angle right" v-else></i>
|
||||
{{cluster.name}}
|
||||
<span class="ui label tiny basic" style="margin-left: 0.5em;">{{cluster.count}}个待升级</span>
|
||||
</div>
|
||||
<div>
|
||||
<button class="ui button tiny primary" v-if="countCheckedNodesInCluster(cluster) > 0"
|
||||
@click.stop.prevent="upgradeBatchInCluster(mod.code, cluster)">批量升级({{countCheckedNodesInCluster(cluster)}})</button>
|
||||
<button class="ui button tiny"
|
||||
@click.stop.prevent="upgradeCluster(mod.code, cluster.id)">升级集群内所有节点</button>
|
||||
</div>
|
||||
</h5>
|
||||
|
||||
<div v-show="cluster.expanded" style="margin-top: 0.5em;">
|
||||
<table class="ui table selectable celled small" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:3em">
|
||||
<div class="ui checkbox" @click.prevent="toggleCheckAll(cluster)">
|
||||
<input type="checkbox" :checked="isAllChecked(cluster)">
|
||||
<label></label>
|
||||
</div>
|
||||
</th>
|
||||
<th>节点名</th>
|
||||
<th>访问IP</th>
|
||||
<th>SSH地址</th>
|
||||
<th>版本变化</th>
|
||||
<th class="four wide">节点状态</th>
|
||||
<th class="two op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr v-for="node in cluster.nodes">
|
||||
<td>
|
||||
<div class="ui checkbox" v-if="!isNodeUpgradeFinished(node)">
|
||||
<input type="checkbox" v-model="node.isChecked">
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{{node.name}}
|
||||
<a :href="nodeDetailURL(mod.code, cluster.id, node.id)" title="节点详情" style="margin-left: 0.4em"><i class="icon external alternate small"></i></a>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="node.accessIP && node.accessIP.length > 0" class="ui label tiny basic">{{node.accessIP}}</span>
|
||||
<span v-else class="disabled">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="node.login != null && node.login.type == 'ssh' && node.loginParams != null && node.loginParams.host != null && node.loginParams.host.length > 0">
|
||||
{{node.loginParams.host}}:{{node.loginParams.port}}
|
||||
</span>
|
||||
<span v-else class="disabled">没有设置</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="node.oldVersion">v{{node.oldVersion}}</span>
|
||||
<span v-else class="ui label tiny">未知</span>
|
||||
-> <strong>v{{node.newVersion}}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="node.installStatus != null && (node.installStatus.isRunning || node.installStatus.isFinished)">
|
||||
<div v-if="node.installStatus.isRunning && !node.installStatus.isFinished"
|
||||
class="blue">
|
||||
<i class="notched circle loading icon"></i> 升级中...
|
||||
</div>
|
||||
<div v-if="node.installStatus.isFinished">
|
||||
<span v-if="node.installStatus.isOk" class="green"><i
|
||||
class="icon check circle"></i>
|
||||
已升级成功</span>
|
||||
<span v-if="!node.installStatus.isOk" class="red"><i
|
||||
class="icon warning circle"></i>
|
||||
升级过程中发生错误:{{node.installStatus.error}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="disabled">等待升级</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="" @click.prevent="upgradeNode(mod.code, node)" v-if="!node.isUpgrading">升级</a>
|
||||
<span v-if="node.isUpgrading" class="blue">升级中...</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
457
EdgeAdmin/web/views/@default/settings/upgrade/index.js
Normal file
457
EdgeAdmin/web/views/@default/settings/upgrade/index.js
Normal file
@@ -0,0 +1,457 @@
|
||||
Tea.context(function () {
|
||||
let that = this
|
||||
|
||||
// 计算总待升级数
|
||||
this.totalUpgradeCount = 0
|
||||
this.modules.forEach(function (mod) {
|
||||
mod.expanded = true
|
||||
that.totalUpgradeCount += mod.count
|
||||
if (mod.clusters != null) {
|
||||
mod.clusters.forEach(function (cluster) {
|
||||
cluster.expanded = true
|
||||
if (cluster.nodes != null) {
|
||||
cluster.nodes.forEach(function (node) {
|
||||
node.isUpgrading = false
|
||||
node.isChecked = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 正在升级的节点ID集合(按模块)
|
||||
this.upgradingNodeIds = {
|
||||
"node": [],
|
||||
"dns": [],
|
||||
"httpdns": []
|
||||
}
|
||||
|
||||
// 启动状态轮询
|
||||
this.$delay(function () {
|
||||
this.pollStatus()
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取节点详情页URL
|
||||
*/
|
||||
this.nodeDetailURL = function (moduleCode, clusterId, nodeId) {
|
||||
switch (moduleCode) {
|
||||
case "node":
|
||||
return "/clusters/cluster/node?clusterId=" + clusterId + "&nodeId=" + nodeId
|
||||
case "dns":
|
||||
return "/ns/clusters/cluster/node?clusterId=" + clusterId + "&nodeId=" + nodeId
|
||||
case "httpdns":
|
||||
return "/httpdns/clusters/cluster/node?clusterId=" + clusterId + "&nodeId=" + nodeId
|
||||
default:
|
||||
return "#"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断节点升级是否已完成(成功)
|
||||
*/
|
||||
this.isNodeUpgradeFinished = function (node) {
|
||||
return node.installStatus != null && node.installStatus.isFinished && node.installStatus.isOk
|
||||
}
|
||||
|
||||
/**
|
||||
* 全选/取消全选
|
||||
*/
|
||||
this.toggleCheckAll = function (cluster) {
|
||||
if (cluster.nodes == null) return
|
||||
let allChecked = that.isAllChecked(cluster)
|
||||
cluster.nodes.forEach(function (node) {
|
||||
if (!that.isNodeUpgradeFinished(node)) {
|
||||
node.isChecked = !allChecked
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否全部选中
|
||||
*/
|
||||
this.isAllChecked = function (cluster) {
|
||||
if (cluster.nodes == null || cluster.nodes.length == 0) return false
|
||||
let checkableNodes = cluster.nodes.filter(function (node) {
|
||||
return !that.isNodeUpgradeFinished(node)
|
||||
})
|
||||
if (checkableNodes.length == 0) return false
|
||||
return checkableNodes.every(function (node) { return node.isChecked })
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算集群内选中的节点数
|
||||
*/
|
||||
this.countCheckedNodesInCluster = function (cluster) {
|
||||
if (cluster.nodes == null) return 0
|
||||
return cluster.nodes.filter(function (node) { return node.isChecked }).length
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部升级
|
||||
*/
|
||||
this.upgradeAll = function () {
|
||||
var vue = this
|
||||
teaweb.confirm("确定要升级所有模块的所有待升级节点吗?", function () {
|
||||
vue.$post("/settings/upgrade/upgradeNode")
|
||||
.params({
|
||||
module: "",
|
||||
scope: "all",
|
||||
clusterId: 0,
|
||||
nodeId: 0
|
||||
})
|
||||
.success(function () {
|
||||
// 标记所有节点为升级中
|
||||
that.modules.forEach(function (mod) {
|
||||
if (mod.clusters != null) {
|
||||
mod.clusters.forEach(function (cluster) {
|
||||
if (cluster.nodes != null) {
|
||||
cluster.nodes.forEach(function (node) {
|
||||
node.installStatus = {
|
||||
isRunning: true,
|
||||
isFinished: false,
|
||||
isOk: false,
|
||||
error: "",
|
||||
errorCode: ""
|
||||
}
|
||||
node.isUpgrading = true
|
||||
that.trackNode(mod.code, node.id)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.fail(function (resp) {
|
||||
teaweb.warn("升级请求失败:" + resp.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 按模块升级
|
||||
*/
|
||||
this.upgradeModule = function (moduleCode) {
|
||||
var vue = this
|
||||
teaweb.confirm("确定要升级该模块的所有待升级节点吗?", function () {
|
||||
vue.$post("/settings/upgrade/upgradeNode")
|
||||
.params({
|
||||
module: moduleCode,
|
||||
scope: "module",
|
||||
clusterId: 0,
|
||||
nodeId: 0
|
||||
})
|
||||
.success(function () {
|
||||
that.markModuleUpgrading(moduleCode)
|
||||
})
|
||||
.fail(function (resp) {
|
||||
teaweb.warn("升级请求失败:" + resp.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 按集群升级
|
||||
*/
|
||||
this.upgradeCluster = function (moduleCode, clusterId) {
|
||||
var vue = this
|
||||
teaweb.confirm("确定要升级该集群的所有待升级节点吗?", function () {
|
||||
vue.$post("/settings/upgrade/upgradeNode")
|
||||
.params({
|
||||
module: moduleCode,
|
||||
scope: "cluster",
|
||||
clusterId: clusterId,
|
||||
nodeId: 0
|
||||
})
|
||||
.success(function () {
|
||||
that.markClusterUpgrading(moduleCode, clusterId)
|
||||
})
|
||||
.fail(function (resp) {
|
||||
teaweb.warn("升级请求失败:" + resp.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量升级集群内选中的节点
|
||||
*/
|
||||
this.upgradeBatchInCluster = function (moduleCode, cluster) {
|
||||
if (cluster.nodes == null) return
|
||||
var checkedNodes = cluster.nodes.filter(function (node) { return node.isChecked })
|
||||
if (checkedNodes.length == 0) return
|
||||
|
||||
var vue = this
|
||||
teaweb.confirm("确定要批量升级选中的 " + checkedNodes.length + " 个节点吗?", function () {
|
||||
checkedNodes.forEach(function (node) {
|
||||
node.installStatus = {
|
||||
isRunning: true,
|
||||
isFinished: false,
|
||||
isOk: false,
|
||||
error: "",
|
||||
errorCode: ""
|
||||
}
|
||||
node.isUpgrading = true
|
||||
node.isChecked = false
|
||||
that.trackNode(moduleCode, node.id)
|
||||
|
||||
vue.$post("/settings/upgrade/upgradeNode")
|
||||
.params({
|
||||
module: moduleCode,
|
||||
scope: "node",
|
||||
clusterId: 0,
|
||||
nodeId: node.id
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 升级单个节点
|
||||
*/
|
||||
this.upgradeNode = function (moduleCode, node) {
|
||||
var vue = this
|
||||
teaweb.confirm("确定要升级节点 \"" + node.name + "\" 吗?", function () {
|
||||
node.installStatus = {
|
||||
isRunning: true,
|
||||
isFinished: false,
|
||||
isOk: false,
|
||||
error: "",
|
||||
errorCode: ""
|
||||
}
|
||||
node.isUpgrading = true
|
||||
that.trackNode(moduleCode, node.id)
|
||||
|
||||
vue.$post("/settings/upgrade/upgradeNode")
|
||||
.params({
|
||||
module: moduleCode,
|
||||
scope: "node",
|
||||
clusterId: 0,
|
||||
nodeId: node.id
|
||||
})
|
||||
.success(function () { })
|
||||
.fail(function (resp) {
|
||||
node.isUpgrading = false
|
||||
teaweb.warn("升级请求失败:" + resp.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记模块下所有节点为升级中
|
||||
*/
|
||||
this.markModuleUpgrading = function (moduleCode) {
|
||||
that.modules.forEach(function (mod) {
|
||||
if (mod.code == moduleCode && mod.clusters != null) {
|
||||
mod.clusters.forEach(function (cluster) {
|
||||
if (cluster.nodes != null) {
|
||||
cluster.nodes.forEach(function (node) {
|
||||
node.installStatus = {
|
||||
isRunning: true,
|
||||
isFinished: false,
|
||||
isOk: false,
|
||||
error: "",
|
||||
errorCode: ""
|
||||
}
|
||||
node.isUpgrading = true
|
||||
that.trackNode(moduleCode, node.id)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记集群下所有节点为升级中
|
||||
*/
|
||||
this.markClusterUpgrading = function (moduleCode, clusterId) {
|
||||
that.modules.forEach(function (mod) {
|
||||
if (mod.code == moduleCode && mod.clusters != null) {
|
||||
mod.clusters.forEach(function (cluster) {
|
||||
if (cluster.id == clusterId && cluster.nodes != null) {
|
||||
cluster.nodes.forEach(function (node) {
|
||||
node.installStatus = {
|
||||
isRunning: true,
|
||||
isFinished: false,
|
||||
isOk: false,
|
||||
error: "",
|
||||
errorCode: ""
|
||||
}
|
||||
node.isUpgrading = true
|
||||
that.trackNode(moduleCode, node.id)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪节点升级状态
|
||||
*/
|
||||
this.trackNode = function (moduleCode, nodeId) {
|
||||
if (that.upgradingNodeIds[moduleCode] == null) {
|
||||
that.upgradingNodeIds[moduleCode] = []
|
||||
}
|
||||
if (that.upgradingNodeIds[moduleCode].indexOf(nodeId) < 0) {
|
||||
that.upgradingNodeIds[moduleCode].push(nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态轮询
|
||||
*/
|
||||
this.pollStatus = function () {
|
||||
var vue = this
|
||||
|
||||
// 检查是否有正在升级的节点
|
||||
let hasUpgrading = false
|
||||
for (let key in that.upgradingNodeIds) {
|
||||
if (that.upgradingNodeIds[key].length > 0) {
|
||||
hasUpgrading = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasUpgrading) {
|
||||
setTimeout(function () { that.pollStatus() }, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
vue.$post("/settings/upgrade/status")
|
||||
.params({
|
||||
nodeIdsJSON: JSON.stringify(that.upgradingNodeIds)
|
||||
})
|
||||
.success(function (resp) {
|
||||
let statuses = resp.data.statuses
|
||||
if (statuses == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新各模块节点状态
|
||||
for (let moduleCode in statuses) {
|
||||
let nodeStatuses = statuses[moduleCode]
|
||||
if (nodeStatuses == null) {
|
||||
continue
|
||||
}
|
||||
nodeStatuses.forEach(function (ns) {
|
||||
that.updateNodeStatus(moduleCode, ns.id, ns.installStatus)
|
||||
})
|
||||
}
|
||||
})
|
||||
.done(function () {
|
||||
setTimeout(function () { that.pollStatus() }, 3000)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新节点安装状态
|
||||
*/
|
||||
this.updateNodeStatus = function (moduleCode, nodeId, installStatus) {
|
||||
that.modules.forEach(function (mod) {
|
||||
if (mod.code == moduleCode && mod.clusters != null) {
|
||||
mod.clusters.forEach(function (cluster) {
|
||||
if (cluster.nodes != null) {
|
||||
cluster.nodes.forEach(function (node) {
|
||||
if (node.id == nodeId && installStatus != null) {
|
||||
node.installStatus = installStatus
|
||||
// 升级完成后移除跟踪
|
||||
if (installStatus.isFinished) {
|
||||
node.isUpgrading = false
|
||||
let idx = that.upgradingNodeIds[moduleCode].indexOf(nodeId)
|
||||
if (idx >= 0) {
|
||||
that.upgradingNodeIds[moduleCode].splice(idx, 1)
|
||||
}
|
||||
|
||||
if (installStatus.isOk) {
|
||||
// 升级成功,延迟后从列表中移除
|
||||
setTimeout(function () {
|
||||
let nIdx = cluster.nodes.indexOf(node)
|
||||
if (nIdx >= 0) {
|
||||
cluster.nodes.splice(nIdx, 1)
|
||||
cluster.count--
|
||||
mod.count--
|
||||
that.totalUpgradeCount--
|
||||
|
||||
if (cluster.count <= 0) {
|
||||
let cIdx = mod.clusters.indexOf(cluster)
|
||||
if (cIdx >= 0) {
|
||||
mod.clusters.splice(cIdx, 1)
|
||||
}
|
||||
}
|
||||
if (mod.count <= 0) {
|
||||
let mIdx = that.modules.indexOf(mod)
|
||||
if (mIdx >= 0) {
|
||||
that.modules.splice(mIdx, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 2000)
|
||||
} else {
|
||||
// 升级失败,根据 errorCode 给出具体提示
|
||||
that.handleUpgradeError(moduleCode, node, installStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理升级错误,根据 errorCode 提供更友好的提示
|
||||
*/
|
||||
this.handleUpgradeError = function (moduleCode, node, installStatus) {
|
||||
let errorCode = installStatus.errorCode || ""
|
||||
let errMsg = installStatus.error || ""
|
||||
|
||||
switch (errorCode) {
|
||||
case "EMPTY_LOGIN":
|
||||
case "EMPTY_SSH_HOST":
|
||||
case "EMPTY_SSH_PORT":
|
||||
case "EMPTY_GRANT":
|
||||
// SSH 信息未配置的错误,不弹窗(页面上已有"没有设置"提示)
|
||||
break
|
||||
case "CREATE_ROOT_DIRECTORY_FAILED":
|
||||
teaweb.warn("节点 \"" + node.name + "\" 创建根目录失败:" + errMsg)
|
||||
break
|
||||
case "INSTALL_HELPER_FAILED":
|
||||
teaweb.warn("节点 \"" + node.name + "\" 安装助手失败:" + errMsg)
|
||||
break
|
||||
case "TEST_FAILED":
|
||||
teaweb.warn("节点 \"" + node.name + "\" 环境测试失败:" + errMsg)
|
||||
break
|
||||
case "RPC_TEST_FAILED":
|
||||
teaweb.warn("节点 \"" + node.name + "\" RPC通讯测试失败:" + errMsg)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新自动升级状态
|
||||
*/
|
||||
this.updateAutoUpgrade = function () {
|
||||
var vue = this
|
||||
|
||||
// @change 已经翻转了值,先记录新值并立即恢复
|
||||
let newValue = that.config.autoUpgrade
|
||||
that.config.autoUpgrade = !newValue
|
||||
|
||||
let msg = newValue ? "确定要开启自动升级功能吗?开启后节点会自动下载安装新版本。" : "确定要关闭自动升级功能吗?关闭后只能通过这里手动执行升级。"
|
||||
teaweb.confirm(msg, function () {
|
||||
vue.$post("/settings/upgrade")
|
||||
.params({
|
||||
autoUpgrade: newValue ? 1 : 0
|
||||
})
|
||||
.success(function () {
|
||||
that.config.autoUpgrade = newValue
|
||||
teaweb.successToast("设置保存成功")
|
||||
})
|
||||
.fail(function (resp) {
|
||||
teaweb.warn("设置保存失败:" + resp.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -39,7 +39,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="hasHTTPDNSFeature">
|
||||
<td>HTTPDNS关联集群</td>
|
||||
<td style="white-space: nowrap">HTTPDNS关联集群</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" name="httpdnsClusterId" v-model="httpdnsClusterId">
|
||||
<option value="0">[未选择]</option>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
.ui.table.definition td.title {
|
||||
min-width: 12em;
|
||||
}
|
||||
.feature-boxes .feature-box {
|
||||
margin-bottom: 1em;
|
||||
width: 24em;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">允许注册</td>
|
||||
<td class="title" style="white-space: nowrap">允许注册</td>
|
||||
<td>
|
||||
<checkbox name="isOn" v-model="config.isOn"></checkbox>
|
||||
<p class="comment">选中表示允许用户自行注册。</p>
|
||||
@@ -40,7 +40,7 @@
|
||||
<h4>登录设置</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">检查客户端区域</td>
|
||||
<td class="title" style="white-space: nowrap">检查客户端区域</td>
|
||||
<td>
|
||||
<checkbox name="checkClientRegion" v-model="config.checkClientRegion"></checkbox>
|
||||
<p class="comment">选中后,表示每次用户访问时都检查客户端所在地理区域是否和登录时一致,以提升安全性;如果当前系统下游有反向代理设置,请在<a
|
||||
@@ -53,7 +53,7 @@
|
||||
<h4>电子邮箱相关</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">启用电子邮箱绑定功能</td>
|
||||
<td class="title" style="white-space: nowrap">启用电子邮箱绑定功能</td>
|
||||
<td>
|
||||
<checkbox name="emailVerificationIsOn" v-model="config.emailVerification.isOn"></checkbox>
|
||||
<p class="comment">选中后,电子邮箱需要激活之后可以使用邮箱登录、找回密码等。此功能需要事先设置 <a href="/users/setting/email"
|
||||
@@ -107,7 +107,7 @@
|
||||
<h4>通过邮箱找回密码</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">启用找回密码功能</td>
|
||||
<td class="title" style="white-space: nowrap">启用找回密码功能</td>
|
||||
<td>
|
||||
<checkbox name="emailResetPasswordIsOn" v-model="config.emailResetPassword.isOn"></checkbox>
|
||||
<p class="comment">选中后,用户可以通过已绑定的电子邮箱找回密码;此功能需要同时开启电子邮箱绑定功能。</p>
|
||||
@@ -146,7 +146,7 @@
|
||||
<h4>手机号码相关</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">启用手机号码绑定功能</td>
|
||||
<td class="title" style="white-space: nowrap">启用手机号码绑定功能</td>
|
||||
<td>
|
||||
<checkbox name="mobileVerificationIsOn" v-model="config.mobileVerification.isOn"></checkbox>
|
||||
<p class="comment">选中后,手机号码需要激活之后可以使用手机号码、找回密码等。此功能需要事先设置 <a href="/users/setting/sms"
|
||||
@@ -196,7 +196,7 @@
|
||||
<h4>CDN服务</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">开通CDN服务</td>
|
||||
<td class="title" style="white-space: nowrap">开通CDN服务</td>
|
||||
<td>
|
||||
<checkbox name="cdnIsOn" v-model="config.cdnIsOn"></checkbox>
|
||||
<p class="comment">选中表示自动为用户开通CDN服务。 </p>
|
||||
@@ -254,7 +254,7 @@
|
||||
<h4>DDoS高防</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">开通DDoS高防管理</td>
|
||||
<td class="title" style="white-space: nowrap">开通DDoS高防管理</td>
|
||||
<td>
|
||||
<checkbox name="adIsOn" v-model="config.adIsOn"></checkbox>
|
||||
<p class="comment">选中表示自动为用户开通DDoS高防IP使用服务。</p>
|
||||
@@ -269,7 +269,7 @@
|
||||
<h4>智能DNS服务</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">开通智能DNS服务</td>
|
||||
<td class="title" style="white-space: nowrap">开通智能DNS服务</td>
|
||||
<td>
|
||||
<checkbox name="nsIsOn" v-model="config.nsIsOn"></checkbox>
|
||||
<p class="comment">选中表示自动为用户开通智能DNS服务。使用默认集群资源,如需使用其他集群,请到用户新增和业务设置页面中进行选择。</p>
|
||||
@@ -284,7 +284,7 @@
|
||||
<h4>HTTPDNS服务</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">开通HTTPDNS服务</td>
|
||||
<td class="title" style="white-space: nowrap">开通HTTPDNS服务</td>
|
||||
<td>
|
||||
<checkbox name="httpdnsIsOn" v-model="config.httpdnsIsOn"></checkbox>
|
||||
<p class="comment">选中表示自动为用户开通HTTPDNS服务。使用默认集群资源,如需使用其他集群,请到用户新增和业务设置页面中进行选择。</p>
|
||||
|
||||
Reference in New Issue
Block a user