Compare commits

...

5 Commits

Author SHA1 Message Date
tiny-craft 30e7016aa3 fix: database index confused after setting database filter 2023-10-08 18:25:52 +08:00
tiny-craft b5dfe377fa feat: add discovery master group name for sentinel mode config
perf: tidy connection profile file
2023-10-08 15:24:08 +08:00
tiny-craft ee68d699fa feat: support sentinel mode #16 #42 2023-10-08 01:33:03 +08:00
tiny-craft 477ed19d20 perf: update version compare logic 2023-10-07 23:37:12 +08:00
tiny-craft d2aa9317b9 chore: update dependencies to latest 2023-10-07 22:15:06 +08:00
16 changed files with 499 additions and 203 deletions

View File

@ -9,15 +9,16 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"net" "net"
"os" "os"
"slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
. "tinyrdm/backend/storage" . "tinyrdm/backend/storage"
"tinyrdm/backend/types" "tinyrdm/backend/types"
"tinyrdm/backend/utils/coll"
maputil "tinyrdm/backend/utils/map" maputil "tinyrdm/backend/utils/map"
redis2 "tinyrdm/backend/utils/redis" redis2 "tinyrdm/backend/utils/redis"
sliceutil "tinyrdm/backend/utils/slice"
strutil "tinyrdm/backend/utils/string" strutil "tinyrdm/backend/utils/string"
) )
@ -74,7 +75,7 @@ func (c *connectionService) Stop(ctx context.Context) {
c.connMap = map[string]connectionItem{} c.connMap = map[string]connectionItem{}
} }
func (c *connectionService) createRedisClient(config types.ConnectionConfig) (*redis.Client, error) { func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.Options, error) {
var sshClient *ssh.Client var sshClient *ssh.Client
if config.SSH.Enable { if config.SSH.Enable {
sshConfig := &ssh.ClientConfig{ sshConfig := &ssh.ClientConfig{
@ -127,10 +128,70 @@ func (c *connectionService) createRedisClient(config types.ConnectionConfig) (*r
option.ReadTimeout = -2 option.ReadTimeout = -2
option.WriteTimeout = -2 option.WriteTimeout = -2
} }
return option, nil
}
func (c *connectionService) createRedisClient(config types.ConnectionConfig) (*redis.Client, error) {
option, err := c.buildOption(config)
if err != nil {
return nil, err
}
if config.Sentinel.Enable {
sentinel := redis.NewSentinelClient(option)
defer sentinel.Close()
var addr []string
addr, err = sentinel.GetMasterAddrByName(c.ctx, config.Sentinel.Master).Result()
if err != nil {
return nil, err
}
if len(addr) < 2 {
return nil, errors.New("cannot get master address")
}
option.Addr = fmt.Sprintf("%s:%s", addr[0], addr[1])
option.Username = config.Sentinel.Username
option.Password = config.Sentinel.Password
}
rdb := redis.NewClient(option) rdb := redis.NewClient(option)
return rdb, nil return rdb, nil
} }
// ListSentinelMasters list all master info by sentinel
func (c *connectionService) ListSentinelMasters(config types.ConnectionConfig) (resp types.JSResp) {
option, err := c.buildOption(config)
if err != nil {
resp.Msg = err.Error()
return
}
if option.DialTimeout > 0 {
option.DialTimeout = 10 * time.Second
}
sentinel := redis.NewSentinelClient(option)
defer sentinel.Close()
var retInfo []map[string]string
masterInfos, err := sentinel.Masters(c.ctx).Result()
if err != nil {
resp.Msg = err.Error()
return
}
for _, info := range masterInfos {
if infoMap, ok := info.(map[any]any); ok {
retInfo = append(retInfo, map[string]string{
"name": infoMap["name"].(string),
"addr": fmt.Sprintf("%s:%s", infoMap["ip"].(string), infoMap["port"].(string)),
})
}
}
resp.Data = retInfo
resp.Success = true
return
}
func (c *connectionService) TestConnection(config types.ConnectionConfig) (resp types.JSResp) { func (c *connectionService) TestConnection(config types.ConnectionConfig) (resp types.JSResp) {
rdb, err := c.createRedisClient(config) rdb, err := c.createRedisClient(config)
if err != nil { if err != nil {
@ -138,7 +199,8 @@ func (c *connectionService) TestConnection(config types.ConnectionConfig) (resp
return return
} }
defer rdb.Close() defer rdb.Close()
if _, err := rdb.Ping(c.ctx).Result(); err != nil && err != redis.Nil {
if _, err = rdb.Ping(c.ctx).Result(); err != nil && err != redis.Nil {
resp.Msg = err.Error() resp.Msg = err.Error()
} else { } else {
resp.Success = true resp.Success = true
@ -291,6 +353,7 @@ func (c *connectionService) OpenConnection(name string) (resp types.JSResp) {
dbInfo := c.parseDBItemInfo(dbInfoStr) dbInfo := c.parseDBItemInfo(dbInfoStr)
return types.ConnectionDB{ return types.ConnectionDB{
Name: dbName, Name: dbName,
Index: idx,
Keys: dbInfo["keys"], Keys: dbInfo["keys"],
Expires: dbInfo["expires"], Expires: dbInfo["expires"],
AvgTTL: dbInfo["avg_ttl"], AvgTTL: dbInfo["avg_ttl"],
@ -298,17 +361,20 @@ func (c *connectionService) OpenConnection(name string) (resp types.JSResp) {
} else { } else {
return types.ConnectionDB{ return types.ConnectionDB{
Name: dbName, Name: dbName,
Index: idx,
} }
} }
} }
switch selConn.DBFilterType { switch selConn.DBFilterType {
case "show": case "show":
for _, idx := range selConn.DBFilterList { filterList := sliceutil.Unique(selConn.DBFilterList)
for _, idx := range filterList {
dbs = append(dbs, queryDB(idx)) dbs = append(dbs, queryDB(idx))
} }
case "hide": case "hide":
hiddenList := coll.NewSet(selConn.DBFilterList...)
for idx := 0; idx < totaldb; idx++ { for idx := 0; idx < totaldb; idx++ {
if !slices.Contains(selConn.DBFilterList, idx) { if !hiddenList.Contains(idx) {
dbs = append(dbs, queryDB(idx)) dbs = append(dbs, queryDB(idx))
} }
} }

View File

@ -37,6 +37,9 @@ func (c *ConnectionsStorage) defaultConnectionItem() types.ConnectionConfig {
DBFilterType: "none", DBFilterType: "none",
DBFilterList: []int{}, DBFilterList: []int{},
MarkColor: "", MarkColor: "",
Sentinel: types.ConnectionSentinel{
Master: "mymaster",
},
} }
} }
@ -192,8 +195,7 @@ func (c *ConnectionsStorage) UpdateConnection(name string, param types.Connectio
updated = true updated = true
} }
} else { } else {
err := retrieve(conn.Connections, name, param) if err := retrieve(conn.Connections, name, param); err != nil {
if err != nil {
return err return err
} }
} }
@ -284,7 +286,7 @@ func (c *ConnectionsStorage) SaveSortedConnection(sortedConns types.Connections)
return c.saveConnections(conns) return c.saveConnections(conns)
} }
// CreateGroup create new group // CreateGroup create a new group
func (c *ConnectionsStorage) CreateGroup(name string) error { func (c *ConnectionsStorage) CreateGroup(name string) error {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
@ -330,7 +332,7 @@ func (c *ConnectionsStorage) RenameGroup(name, newName string) error {
return c.saveConnections(conns) return c.saveConnections(conns)
} }
// DeleteGroup remove special group, include all connections under it // DeleteGroup remove specified group, include all connections under it
func (c *ConnectionsStorage) DeleteGroup(group string, includeConnection bool) error { func (c *ConnectionsStorage) DeleteGroup(group string, includeConnection bool) error {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()

View File

@ -17,6 +17,7 @@ type ConnectionConfig struct {
DBFilterList []int `json:"dbFilterList" yaml:"db_filter_list,omitempty"` DBFilterList []int `json:"dbFilterList" yaml:"db_filter_list,omitempty"`
MarkColor string `json:"markColor,omitempty" yaml:"mark_color,omitempty"` MarkColor string `json:"markColor,omitempty" yaml:"mark_color,omitempty"`
SSH ConnectionSSH `json:"ssh,omitempty" yaml:"ssh,omitempty"` SSH ConnectionSSH `json:"ssh,omitempty" yaml:"ssh,omitempty"`
Sentinel ConnectionSentinel `json:"sentinel,omitempty" yaml:"sentinel,omitempty"`
} }
type Connection struct { type Connection struct {
@ -34,6 +35,7 @@ type ConnectionGroup struct {
type ConnectionDB struct { type ConnectionDB struct {
Name string `json:"name"` Name string `json:"name"`
Index int `json:"index"`
Keys int `json:"keys"` Keys int `json:"keys"`
Expires int `json:"expires,omitempty"` Expires int `json:"expires,omitempty"`
AvgTTL int `json:"avgTtl,omitempty"` AvgTTL int `json:"avgTtl,omitempty"`
@ -44,8 +46,15 @@ type ConnectionSSH struct {
Addr string `json:"addr,omitempty" yaml:"addr,omitempty"` Addr string `json:"addr,omitempty" yaml:"addr,omitempty"`
Port int `json:"port,omitempty" yaml:"port,omitempty"` Port int `json:"port,omitempty" yaml:"port,omitempty"`
LoginType string `json:"loginType" yaml:"login_type"` LoginType string `json:"loginType" yaml:"login_type"`
Username string `json:"username" yaml:"username"` Username string `json:"username" yaml:"username,omitempty"`
Password string `json:"password,omitempty" yaml:"password,omitempty"` Password string `json:"password,omitempty" yaml:"password,omitempty"`
PKFile string `json:"pkFile,omitempty" yaml:"pk_file,omitempty"` PKFile string `json:"pkFile,omitempty" yaml:"pk_file,omitempty"`
Passphrase string `json:"passphrase,omitempty" yaml:"passphrase,omitempty"` Passphrase string `json:"passphrase,omitempty" yaml:"passphrase,omitempty"`
} }
type ConnectionSentinel struct {
Enable bool `json:"enable" yaml:"enable"`
Master string `json:"master" yaml:"master"`
Username string `json:"username,omitempty" yaml:"username,omitempty"`
Password string `json:"password,omitempty" yaml:"password,omitempty"`
}

View File

@ -178,11 +178,11 @@ func decodeBinary(str string) (string, bool) {
} }
func decodeHex(str string) (string, bool) { func decodeHex(str string) (string, bool) {
encodeStr := hex.EncodeToString([]byte(str)) decodeStr := hex.EncodeToString([]byte(str))
var resultStr strings.Builder var resultStr strings.Builder
for i := 0; i < len(encodeStr); i += 2 { for i := 0; i < len(decodeStr); i += 2 {
resultStr.WriteString("\\x") resultStr.WriteString("\\x")
resultStr.WriteString(encodeStr[i : i+2]) resultStr.WriteString(decodeStr[i : i+2])
} }
return resultStr.String(), true return resultStr.String(), true
} }

View File

@ -12,18 +12,18 @@
"highlight.js": "^11.8.0", "highlight.js": "^11.8.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"sass": "^1.68.0", "sass": "^1.69.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-i18n": "^9.4.1" "vue-i18n": "^9.5.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.3.4", "@vitejs/plugin-vue": "^4.4.0",
"naive-ui": "^2.34.4", "naive-ui": "^2.35.0",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"unplugin-auto-import": "^0.16.6", "unplugin-auto-import": "^0.16.6",
"unplugin-icons": "^0.17.0", "unplugin-icons": "^0.17.0",
"unplugin-vue-components": "^0.25.2", "unplugin-vue-components": "^0.25.2",
"vite": "^4.4.9" "vite": "^4.4.11"
} }
}, },
"node_modules/@antfu/install-pkg": { "node_modules/@antfu/install-pkg": {
@ -54,9 +54,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.22.15", "version": "7.23.1",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.22.15.tgz", "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.23.1.tgz",
"integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
@ -462,23 +462,23 @@
} }
}, },
"node_modules/@intlify/core-base": { "node_modules/@intlify/core-base": {
"version": "9.4.1", "version": "9.5.0",
"resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.4.1.tgz", "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.5.0.tgz",
"integrity": "sha512-WIwx+elsZbxSMxRG5+LC+utRohFvmZMoDevfKOfnYMLbpCjCSavqTfHJAtfsY6ruowzqXeKkeLhRHbYbjoJx5g==", "integrity": "sha512-y3ufM1RJbI/DSmJf3lYs9ACq3S/iRvaSsE3rPIk0MGH7fp+JxU6rdryv/EYcwfcr3Y1aHFlCBir6S391hRZ57w==",
"dependencies": { "dependencies": {
"@intlify/message-compiler": "9.4.1", "@intlify/message-compiler": "9.5.0",
"@intlify/shared": "9.4.1" "@intlify/shared": "9.5.0"
}, },
"engines": { "engines": {
"node": ">= 16" "node": ">= 16"
} }
}, },
"node_modules/@intlify/message-compiler": { "node_modules/@intlify/message-compiler": {
"version": "9.4.1", "version": "9.5.0",
"resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.4.1.tgz", "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.5.0.tgz",
"integrity": "sha512-aN2N+dUx320108QhH51Ycd2LEpZ+NKbzyQ2kjjhqMcxhHdxtOnkgdx+MDBhOy/CObwBmhC3Nygzc6hNlfKvPNw==", "integrity": "sha512-CAhVNfEZcOVFg0/5MNyt+OFjvs4J/ARjCj2b+54/FvFP0EDJI5lIqMTSDBE7k0atMROSP0SvWCkwu/AZ5xkK1g==",
"dependencies": { "dependencies": {
"@intlify/shared": "9.4.1", "@intlify/shared": "9.5.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
}, },
"engines": { "engines": {
@ -486,9 +486,9 @@
} }
}, },
"node_modules/@intlify/shared": { "node_modules/@intlify/shared": {
"version": "9.4.1", "version": "9.5.0",
"resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.4.1.tgz", "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.5.0.tgz",
"integrity": "sha512-A51elBmZWf1FS80inf/32diO9DeXoqg9GR9aUDHFcfHoNDuT46Q+fpPOdj8jiJnSHSBh8E1E+6qWRhAZXdK3Ng==", "integrity": "sha512-tAxV14LMXZDZbu32XzLMTsowNlgJNmLwWHYzvMUl6L8gvQeoYiZONjY7AUsqZW8TOZDX9lfvF6adPkk9FSRdDA==",
"engines": { "engines": {
"node": ">= 16" "node": ">= 16"
} }
@ -568,9 +568,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/katex": { "node_modules/@types/katex": {
"version": "0.14.0", "version": "0.16.3",
"resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.14.0.tgz", "resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.16.3.tgz",
"integrity": "sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==", "integrity": "sha512-CeVMX9EhVUW8MWnei05eIRks4D5Wscw/W9Byz1s3PA+yJvcdvq9SaDjiUKvRvEgjpdTyJMjQA43ae4KTwsvOPg==",
"dev": true "dev": true
}, },
"node_modules/@types/lodash": { "node_modules/@types/lodash": {
@ -589,9 +589,9 @@
} }
}, },
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "4.3.4", "version": "4.4.0",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.3.4.tgz", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz",
"integrity": "sha512-ciXNIHKPriERBisHFBvnTbfKa6r9SAesOYXeGDzgegcvy9Q4xdScSHAmKbNT0M3O0S9LKhIf5/G+UYG4NnnzYw==", "integrity": "sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^14.18.0 || >=16.0.0" "node": "^14.18.0 || >=16.0.0"
@ -840,9 +840,9 @@
} }
}, },
"node_modules/date-fns-tz": { "node_modules/date-fns-tz": {
"version": "1.3.8", "version": "2.0.0",
"resolved": "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-1.3.8.tgz", "resolved": "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-2.0.0.tgz",
"integrity": "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==", "integrity": "sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==",
"dev": true, "dev": true,
"peerDependencies": { "peerDependencies": {
"date-fns": ">=2.0.0" "date-fns": ">=2.0.0"
@ -1254,22 +1254,22 @@
"dev": true "dev": true
}, },
"node_modules/naive-ui": { "node_modules/naive-ui": {
"version": "2.34.4", "version": "2.35.0",
"resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.34.4.tgz", "resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.35.0.tgz",
"integrity": "sha512-aPG8PDfhSzIzn/jSC9y3Jb3Pe2wHJ7F0cFV1EWlbImSrZECeUmoc+fIcOSWbizoztkKfaUAeKwYdMl09MKkj1g==", "integrity": "sha512-PdnLpOip1LQaKs5+rXLZoPDPQkTq26TnHWeABvUA2eOQjtHxE4+TQvj0Jq/W8clM2On/7jptoGmenLt48G3Bhg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@css-render/plugin-bem": "^0.15.10", "@css-render/plugin-bem": "^0.15.12",
"@css-render/vue3-ssr": "^0.15.10", "@css-render/vue3-ssr": "^0.15.12",
"@types/katex": "^0.14.0", "@types/katex": "^0.16.2",
"@types/lodash": "^4.14.181", "@types/lodash": "^4.14.198",
"@types/lodash-es": "^4.17.6", "@types/lodash-es": "^4.17.9",
"async-validator": "^4.0.7", "async-validator": "^4.2.5",
"css-render": "^0.15.10", "css-render": "^0.15.12",
"date-fns": "^2.28.0", "date-fns": "^2.30.0",
"date-fns-tz": "^1.3.3", "date-fns-tz": "^2.0.0",
"evtd": "^0.2.4", "evtd": "^0.2.4",
"highlight.js": "^11.5.0", "highlight.js": "^11.8.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"seemly": "^0.3.6", "seemly": "^0.3.6",
@ -1545,9 +1545,9 @@
} }
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.68.0", "version": "1.69.0",
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.68.0.tgz", "resolved": "https://registry.npmmirror.com/sass/-/sass-1.69.0.tgz",
"integrity": "sha512-Lmj9lM/fef0nQswm1J2HJcEsBUba4wgNx2fea6yJHODREoMFnwRpZydBnX/RjyXw2REIwdkbqE4hrTo4qfDBUA==", "integrity": "sha512-l3bbFpfTOGgQZCLU/gvm1lbsQ5mC/WnLz3djL2v4WCJBDrWm58PO+jgngcGRNnKUh6wSsdm50YaovTqskZ0xDQ==",
"dependencies": { "dependencies": {
"chokidar": ">=3.0.0 <4.0.0", "chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0", "immutable": "^4.0.0",
@ -1805,9 +1805,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.4.9", "version": "4.4.11",
"resolved": "https://registry.npmmirror.com/vite/-/vite-4.4.9.tgz", "resolved": "https://registry.npmmirror.com/vite/-/vite-4.4.11.tgz",
"integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", "integrity": "sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",
@ -1881,12 +1881,12 @@
} }
}, },
"node_modules/vue-i18n": { "node_modules/vue-i18n": {
"version": "9.4.1", "version": "9.5.0",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.4.1.tgz", "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.5.0.tgz",
"integrity": "sha512-vnQyYE9LBuNOqPpETIcCaGnAyLEqfeIvDcyZ9T+WBCWFTqWw1J8FuF1jfeDwpHBi5JKgAwgXyq1mt8jp/x/GPA==", "integrity": "sha512-NiI3Ph1qMstNf7uhYh8trQBOBFLxeJgcOxBq51pCcZ28Vs18Y7BDS58r8HGDKCYgXdLUYqPDXdKatIF4bvBVZg==",
"dependencies": { "dependencies": {
"@intlify/core-base": "9.4.1", "@intlify/core-base": "9.5.0",
"@intlify/shared": "9.4.1", "@intlify/shared": "9.5.0",
"@vue/devtools-api": "^6.5.0" "@vue/devtools-api": "^6.5.0"
}, },
"engines": { "engines": {
@ -1977,9 +1977,9 @@
"integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==" "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA=="
}, },
"@babel/runtime": { "@babel/runtime": {
"version": "7.22.15", "version": "7.23.1",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.22.15.tgz", "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.23.1.tgz",
"integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==",
"dev": true, "dev": true,
"requires": { "requires": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
@ -2180,27 +2180,27 @@
} }
}, },
"@intlify/core-base": { "@intlify/core-base": {
"version": "9.4.1", "version": "9.5.0",
"resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.4.1.tgz", "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.5.0.tgz",
"integrity": "sha512-WIwx+elsZbxSMxRG5+LC+utRohFvmZMoDevfKOfnYMLbpCjCSavqTfHJAtfsY6ruowzqXeKkeLhRHbYbjoJx5g==", "integrity": "sha512-y3ufM1RJbI/DSmJf3lYs9ACq3S/iRvaSsE3rPIk0MGH7fp+JxU6rdryv/EYcwfcr3Y1aHFlCBir6S391hRZ57w==",
"requires": { "requires": {
"@intlify/message-compiler": "9.4.1", "@intlify/message-compiler": "9.5.0",
"@intlify/shared": "9.4.1" "@intlify/shared": "9.5.0"
} }
}, },
"@intlify/message-compiler": { "@intlify/message-compiler": {
"version": "9.4.1", "version": "9.5.0",
"resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.4.1.tgz", "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.5.0.tgz",
"integrity": "sha512-aN2N+dUx320108QhH51Ycd2LEpZ+NKbzyQ2kjjhqMcxhHdxtOnkgdx+MDBhOy/CObwBmhC3Nygzc6hNlfKvPNw==", "integrity": "sha512-CAhVNfEZcOVFg0/5MNyt+OFjvs4J/ARjCj2b+54/FvFP0EDJI5lIqMTSDBE7k0atMROSP0SvWCkwu/AZ5xkK1g==",
"requires": { "requires": {
"@intlify/shared": "9.4.1", "@intlify/shared": "9.5.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }
}, },
"@intlify/shared": { "@intlify/shared": {
"version": "9.4.1", "version": "9.5.0",
"resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.4.1.tgz", "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.5.0.tgz",
"integrity": "sha512-A51elBmZWf1FS80inf/32diO9DeXoqg9GR9aUDHFcfHoNDuT46Q+fpPOdj8jiJnSHSBh8E1E+6qWRhAZXdK3Ng==" "integrity": "sha512-tAxV14LMXZDZbu32XzLMTsowNlgJNmLwWHYzvMUl6L8gvQeoYiZONjY7AUsqZW8TOZDX9lfvF6adPkk9FSRdDA=="
}, },
"@jridgewell/sourcemap-codec": { "@jridgewell/sourcemap-codec": {
"version": "1.4.15", "version": "1.4.15",
@ -2257,9 +2257,9 @@
"dev": true "dev": true
}, },
"@types/katex": { "@types/katex": {
"version": "0.14.0", "version": "0.16.3",
"resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.14.0.tgz", "resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.16.3.tgz",
"integrity": "sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==", "integrity": "sha512-CeVMX9EhVUW8MWnei05eIRks4D5Wscw/W9Byz1s3PA+yJvcdvq9SaDjiUKvRvEgjpdTyJMjQA43ae4KTwsvOPg==",
"dev": true "dev": true
}, },
"@types/lodash": { "@types/lodash": {
@ -2278,9 +2278,9 @@
} }
}, },
"@vitejs/plugin-vue": { "@vitejs/plugin-vue": {
"version": "4.3.4", "version": "4.4.0",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.3.4.tgz", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz",
"integrity": "sha512-ciXNIHKPriERBisHFBvnTbfKa6r9SAesOYXeGDzgegcvy9Q4xdScSHAmKbNT0M3O0S9LKhIf5/G+UYG4NnnzYw==", "integrity": "sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==",
"dev": true, "dev": true,
"requires": {} "requires": {}
}, },
@ -2496,9 +2496,9 @@
} }
}, },
"date-fns-tz": { "date-fns-tz": {
"version": "1.3.8", "version": "2.0.0",
"resolved": "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-1.3.8.tgz", "resolved": "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-2.0.0.tgz",
"integrity": "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==", "integrity": "sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==",
"dev": true, "dev": true,
"requires": {} "requires": {}
}, },
@ -2820,22 +2820,22 @@
"dev": true "dev": true
}, },
"naive-ui": { "naive-ui": {
"version": "2.34.4", "version": "2.35.0",
"resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.34.4.tgz", "resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.35.0.tgz",
"integrity": "sha512-aPG8PDfhSzIzn/jSC9y3Jb3Pe2wHJ7F0cFV1EWlbImSrZECeUmoc+fIcOSWbizoztkKfaUAeKwYdMl09MKkj1g==", "integrity": "sha512-PdnLpOip1LQaKs5+rXLZoPDPQkTq26TnHWeABvUA2eOQjtHxE4+TQvj0Jq/W8clM2On/7jptoGmenLt48G3Bhg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@css-render/plugin-bem": "^0.15.10", "@css-render/plugin-bem": "^0.15.12",
"@css-render/vue3-ssr": "^0.15.10", "@css-render/vue3-ssr": "^0.15.12",
"@types/katex": "^0.14.0", "@types/katex": "^0.16.2",
"@types/lodash": "^4.14.181", "@types/lodash": "^4.14.198",
"@types/lodash-es": "^4.17.6", "@types/lodash-es": "^4.17.9",
"async-validator": "^4.0.7", "async-validator": "^4.2.5",
"css-render": "^0.15.10", "css-render": "^0.15.12",
"date-fns": "^2.28.0", "date-fns": "^2.30.0",
"date-fns-tz": "^1.3.3", "date-fns-tz": "^2.0.0",
"evtd": "^0.2.4", "evtd": "^0.2.4",
"highlight.js": "^11.5.0", "highlight.js": "^11.8.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"seemly": "^0.3.6", "seemly": "^0.3.6",
@ -3025,9 +3025,9 @@
} }
}, },
"sass": { "sass": {
"version": "1.68.0", "version": "1.69.0",
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.68.0.tgz", "resolved": "https://registry.npmmirror.com/sass/-/sass-1.69.0.tgz",
"integrity": "sha512-Lmj9lM/fef0nQswm1J2HJcEsBUba4wgNx2fea6yJHODREoMFnwRpZydBnX/RjyXw2REIwdkbqE4hrTo4qfDBUA==", "integrity": "sha512-l3bbFpfTOGgQZCLU/gvm1lbsQ5mC/WnLz3djL2v4WCJBDrWm58PO+jgngcGRNnKUh6wSsdm50YaovTqskZ0xDQ==",
"requires": { "requires": {
"chokidar": ">=3.0.0 <4.0.0", "chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0", "immutable": "^4.0.0",
@ -3203,9 +3203,9 @@
} }
}, },
"vite": { "vite": {
"version": "4.4.9", "version": "4.4.11",
"resolved": "https://registry.npmmirror.com/vite/-/vite-4.4.9.tgz", "resolved": "https://registry.npmmirror.com/vite/-/vite-4.4.11.tgz",
"integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", "integrity": "sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==",
"dev": true, "dev": true,
"requires": { "requires": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",
@ -3236,12 +3236,12 @@
} }
}, },
"vue-i18n": { "vue-i18n": {
"version": "9.4.1", "version": "9.5.0",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.4.1.tgz", "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.5.0.tgz",
"integrity": "sha512-vnQyYE9LBuNOqPpETIcCaGnAyLEqfeIvDcyZ9T+WBCWFTqWw1J8FuF1jfeDwpHBi5JKgAwgXyq1mt8jp/x/GPA==", "integrity": "sha512-NiI3Ph1qMstNf7uhYh8trQBOBFLxeJgcOxBq51pCcZ28Vs18Y7BDS58r8HGDKCYgXdLUYqPDXdKatIF4bvBVZg==",
"requires": { "requires": {
"@intlify/core-base": "9.4.1", "@intlify/core-base": "9.5.0",
"@intlify/shared": "9.4.1", "@intlify/shared": "9.5.0",
"@vue/devtools-api": "^6.5.0" "@vue/devtools-api": "^6.5.0"
} }
}, },

View File

@ -13,17 +13,17 @@
"highlight.js": "^11.8.0", "highlight.js": "^11.8.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"sass": "^1.68.0", "sass": "^1.69.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-i18n": "^9.4.1" "vue-i18n": "^9.5.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.3.4", "@vitejs/plugin-vue": "^4.4.0",
"naive-ui": "^2.34.4", "naive-ui": "^2.35.0",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"unplugin-auto-import": "^0.16.6", "unplugin-auto-import": "^0.16.6",
"unplugin-icons": "^0.17.0", "unplugin-icons": "^0.17.0",
"unplugin-vue-components": "^0.25.2", "unplugin-vue-components": "^0.25.2",
"vite": "^4.4.9" "vite": "^4.4.11"
} }
} }

View File

@ -1 +1 @@
a65375421b9b10cadef51ed8edc1c6f1 82f42b67ae979cb1af64c05c79c5251f

View File

@ -2,7 +2,7 @@
import { every, get, includes, isEmpty, map, sortBy, toNumber } from 'lodash' import { every, get, includes, isEmpty, map, sortBy, toNumber } from 'lodash'
import { computed, nextTick, ref, watch } from 'vue' import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { SelectKeyFile, TestConnection } from 'wailsjs/go/services/connectionService.js' import { ListSentinelMasters, SelectKeyFile, TestConnection } from 'wailsjs/go/services/connectionService.js'
import useDialog, { ConnDialogType } from 'stores/dialog' import useDialog, { ConnDialogType } from 'stores/dialog'
import Close from '@/components/icons/Close.vue' import Close from '@/components/icons/Close.vue'
import useConnectionStore from 'stores/connections.js' import useConnectionStore from 'stores/connections.js'
@ -57,16 +57,26 @@ const groupOptions = computed(() => {
}) })
const dbFilterList = ref([]) const dbFilterList = ref([])
const onUpdateDBFilterList = (list) => { const onUpdateDBFilterType = (t) => {
const dbList = [] if (t !== 'none') {
for (const item of list) { // set default filter index if empty
if (isEmpty(dbFilterList.value)) {
dbFilterList.value = ['0']
}
}
}
watch(
() => dbFilterList.value,
(list) => {
const dbList = map(list, (item) => {
const idx = toNumber(item) const idx = toNumber(item)
if (!isNaN(idx)) { return isNaN(idx) ? 0 : idx
dbList.push(idx) })
}
}
generalForm.value.dbFilterList = sortBy(dbList) generalForm.value.dbFilterList = sortBy(dbList)
} },
{ deep: true },
)
const sshLoginType = computed(() => { const sshLoginType = computed(() => {
return get(generalForm.value, 'ssh.loginType', 'pwd') return get(generalForm.value, 'ssh.loginType', 'pwd')
@ -81,6 +91,36 @@ const onChoosePKFile = async () => {
} }
} }
const loadingSentinelMaster = ref(false)
const masterNameOptions = ref([])
const onLoadSentinelMasters = async () => {
try {
loadingSentinelMaster.value = true
const { success, data, msg } = await ListSentinelMasters(generalForm.value)
if (!success || isEmpty(data)) {
$message.error(msg || 'list sentinel master fail')
} else {
const options = []
for (const m of data) {
options.push({
label: m['name'],
value: m['name'],
})
}
// select default names
if (!isEmpty(options)) {
generalForm.value.sentinel.master = options[0].value
}
masterNameOptions.value = options
}
} catch (e) {
$message.error(e.message)
} finally {
loadingSentinelMaster.value = false
}
}
const tab = ref('general') const tab = ref('general')
const testing = ref(false) const testing = ref(false)
const showTestResult = ref(false) const showTestResult = ref(false)
@ -104,6 +144,11 @@ const onSaveConnection = async () => {
} }
}) })
// trim advance data
if (get(generalForm.value, 'dbFilterType', 'none') === 'none') {
generalForm.value.dbFilterList = []
}
// trim ssh login data // trim ssh login data
if (generalForm.value.ssh.enable) { if (generalForm.value.ssh.enable) {
switch (generalForm.value.ssh.loginType) { switch (generalForm.value.ssh.loginType) {
@ -121,6 +166,13 @@ const onSaveConnection = async () => {
generalForm.value.ssh = ssh generalForm.value.ssh = ssh
} }
// trim sentinel data
if (!generalForm.value.sentinel.enable) {
generalForm.value.sentinel.master = ''
generalForm.value.sentinel.username = ''
generalForm.value.sentinel.password = ''
}
// store new connection // store new connection
const { success, msg } = await connectionStore.saveConnection( const { success, msg } = await connectionStore.saveConnection(
isEditMode.value ? editName.value : null, isEditMode.value ? editName.value : null,
@ -142,6 +194,7 @@ const resetForm = () => {
showTestResult.value = false showTestResult.value = false
testResult.value = '' testResult.value = ''
tab.value = 'general' tab.value = 'general'
loadingSentinelMaster.value = false
} }
watch( watch(
@ -198,6 +251,7 @@ const onClose = () => {
transform-origin="center"> transform-origin="center">
<n-spin :show="closingConnection"> <n-spin :show="closingConnection">
<n-tabs v-model:value="tab" animated type="line"> <n-tabs v-model:value="tab" animated type="line">
<!-- General pane -->
<n-tab-pane :tab="$t('dialogue.connection.general')" display-directive="show" name="general"> <n-tab-pane :tab="$t('dialogue.connection.general')" display-directive="show" name="general">
<n-form <n-form
ref="generalFormRef" ref="generalFormRef"
@ -239,7 +293,8 @@ const onClose = () => {
</n-form> </n-form>
</n-tab-pane> </n-tab-pane>
<n-tab-pane :tab="$t('dialogue.connection.advanced')" display-directive="show" name="advanced"> <!-- Advance pane -->
<n-tab-pane :tab="$t('dialogue.connection.advn.title')" display-directive="show" name="advanced">
<n-form <n-form
ref="advanceFormRef" ref="advanceFormRef"
:model="generalForm" :model="generalForm"
@ -284,7 +339,9 @@ const onClose = () => {
</n-input-number> </n-input-number>
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="24" :label="$t('dialogue.connection.advn.dbfilter_type')"> <n-form-item-gi :span="24" :label="$t('dialogue.connection.advn.dbfilter_type')">
<n-radio-group v-model:value="generalForm.dbFilterType"> <n-radio-group
v-model:value="generalForm.dbFilterType"
@update:value="onUpdateDBFilterType">
<n-radio-button :label="$t('dialogue.connection.advn.dbfilter_all')" value="none" /> <n-radio-button :label="$t('dialogue.connection.advn.dbfilter_all')" value="none" />
<n-radio-button <n-radio-button
:label="$t('dialogue.connection.advn.dbfilter_show')" :label="$t('dialogue.connection.advn.dbfilter_show')"
@ -294,7 +351,10 @@ const onClose = () => {
value="hide" /> value="hide" />
</n-radio-group> </n-radio-group>
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi :span="24" :label="$t('dialogue.connection.advn.dbfilter_input')"> <n-form-item-gi
v-show="generalForm.dbFilterType !== 'none'"
:span="24"
:label="$t('dialogue.connection.advn.dbfilter_input')">
<n-select <n-select
v-model:value="dbFilterList" v-model:value="dbFilterList"
:disabled="generalForm.dbFilterType === 'none'" :disabled="generalForm.dbFilterType === 'none'"
@ -304,8 +364,7 @@ const onClose = () => {
:placeholder="$t('dialogue.connection.advn.dbfilter_input_tip')" :placeholder="$t('dialogue.connection.advn.dbfilter_input_tip')"
:show-arrow="false" :show-arrow="false"
:show="false" :show="false"
:clearable="true" :clearable="true" />
@update:value="onUpdateDBFilterList" />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi <n-form-item-gi
:span="24" :span="24"
@ -327,14 +386,14 @@ const onClose = () => {
</n-form> </n-form>
</n-tab-pane> </n-tab-pane>
<n-tab-pane :tab="$t('dialogue.connection.ssh.tunnel')" display-directive="show" name="ssh"> <!-- SSH pane -->
<n-tab-pane :tab="$t('dialogue.connection.ssh.title')" display-directive="show" name="ssh">
<n-form-item label-placement="left"> <n-form-item label-placement="left">
<n-checkbox v-model:checked="generalForm.ssh.enable" size="medium"> <n-checkbox v-model:checked="generalForm.ssh.enable" size="medium">
{{ $t('dialogue.connection.ssh.enable') }} {{ $t('dialogue.connection.ssh.enable') }}
</n-checkbox> </n-checkbox>
</n-form-item> </n-form-item>
<n-form <n-form
ref="sshFormRef"
:model="generalForm.ssh" :model="generalForm.ssh"
:show-require-mark="false" :show-require-mark="false"
:disabled="!generalForm.ssh.enable" :disabled="!generalForm.ssh.enable"
@ -388,8 +447,46 @@ const onClose = () => {
</n-form> </n-form>
</n-tab-pane> </n-tab-pane>
<!-- Sentinel pane -->
<n-tab-pane :tab="$t('dialogue.connection.sentinel.title')" display-directive="show" name="sentinel">
<n-form-item label-placement="left">
<n-checkbox v-model:checked="generalForm.sentinel.enable" size="medium">
{{ $t('dialogue.connection.sentinel.enable') }}
</n-checkbox>
</n-form-item>
<n-form
:model="generalForm.sentinel"
:show-require-mark="false"
:disabled="!generalForm.sentinel.enable"
label-placement="top">
<n-form-item :label="$t('dialogue.connection.sentinel.master')">
<n-input-group>
<n-select
v-model:value="generalForm.sentinel.master"
filterable
tag
:options="masterNameOptions" />
<n-button :loading="loadingSentinelMaster" @click="onLoadSentinelMasters">
{{ $t('dialogue.connection.sentinel.auto_discover') }}
</n-button>
</n-input-group>
</n-form-item>
<n-form-item :label="$t('dialogue.connection.sentinel.password')">
<n-input
v-model:value="generalForm.sentinel.password"
:placeholder="$t('dialogue.connection.sentinel.pwd_tip')"
show-password-on="click"
type="password" />
</n-form-item>
<n-form-item :label="$t('dialogue.connection.sentinel.username')">
<n-input
v-model:value="generalForm.sentinel.username"
:placeholder="$t('dialogue.connection.sentinel.usr_tip')" />
</n-form-item>
</n-form>
</n-tab-pane>
<!-- TODO: SSL tab pane --> <!-- TODO: SSL tab pane -->
<!-- TODO: Sentinel tab pane -->
<!-- TODO: Cluster tab pane --> <!-- TODO: Cluster tab pane -->
</n-tabs> </n-tabs>

View File

@ -114,7 +114,6 @@
"new_title": "New Connection", "new_title": "New Connection",
"edit_title": "Edit Connection", "edit_title": "Edit Connection",
"general": "General", "general": "General",
"advanced": "Advanced",
"no_group": "No Group", "no_group": "No Group",
"group": "Group", "group": "Group",
"conn_name": "Name", "conn_name": "Name",
@ -123,12 +122,13 @@
"pwd": "Password", "pwd": "Password",
"name_tip": "Connection name", "name_tip": "Connection name",
"addr_tip": "Redis server host", "addr_tip": "Redis server host",
"usr_tip": "(Optional) Redis server username", "usr_tip": "(Optional) Authentication username",
"pwd_tip": "(Optional) Redis server authentication password (Redis > 6.0)", "pwd_tip": "(Optional) Authentication password (Redis > 6.0)",
"test": "Test Connection", "test": "Test Connection",
"test_succ": "Successful connection to redis-server", "test_succ": "Successful connection to redis-server",
"test_fail": "Fail Connection", "test_fail": "Fail Connection",
"advn": { "advn": {
"title": "Advanced",
"filter": "Default Key Filter Pattern", "filter": "Default Key Filter Pattern",
"filter_tip": "Pattern which defines loaded keys from redis server", "filter_tip": "Pattern which defines loaded keys from redis server",
"separator": "Key Separator", "separator": "Key Separator",
@ -146,8 +146,8 @@
"mark_color": "Mark Color" "mark_color": "Mark Color"
}, },
"ssh": { "ssh": {
"title": "SSH Tunnel",
"enable": "Enable SSH Tuntel", "enable": "Enable SSH Tuntel",
"tunnel": "SSH Tunnel",
"login_type": "Login Type", "login_type": "Login Type",
"pkfile": "Private Key File", "pkfile": "Private Key File",
"passphrase": "Passphrase", "passphrase": "Passphrase",
@ -157,6 +157,16 @@
"pkfile_tip": "SSH Private Key File Path", "pkfile_tip": "SSH Private Key File Path",
"passphrase_tip": "(Optional) Passphrase for Private Key", "passphrase_tip": "(Optional) Passphrase for Private Key",
"pkfile_selection_title": "Please Select Private Key File" "pkfile_selection_title": "Please Select Private Key File"
},
"sentinel": {
"title": "Sentinel",
"enable": "Serve as Sentinel Node",
"master": "Master Group Name",
"auto_discover": "Auto Discover",
"password": "Password for Master Node",
"username": "Username for Master Node",
"pwd_tip": "(Optional) Authentication username for master node",
"usr_tip": "(Optional) Authentication password for master node (Redis > 6.0)"
} }
}, },
"group": { "group": {

View File

@ -114,7 +114,6 @@
"new_title": "新建连接", "new_title": "新建连接",
"edit_title": "编辑连接", "edit_title": "编辑连接",
"general": "常规配置", "general": "常规配置",
"advanced": "高级配置",
"no_group": "无分组", "no_group": "无分组",
"group": "分组", "group": "分组",
"conn_name": "连接名", "conn_name": "连接名",
@ -129,6 +128,7 @@
"test_succ": "成功连接到Redis服务器", "test_succ": "成功连接到Redis服务器",
"test_fail": "连接失败", "test_fail": "连接失败",
"advn": { "advn": {
"title": "高级配置",
"filter": "默认键过滤表达式", "filter": "默认键过滤表达式",
"filter_tip": "需要加载的键名表达式", "filter_tip": "需要加载的键名表达式",
"separator": "键分隔符", "separator": "键分隔符",
@ -147,7 +147,7 @@
}, },
"ssh": { "ssh": {
"enable": "启用SSH隧道", "enable": "启用SSH隧道",
"tunnel": "SSH隧道", "title": "SSH隧道",
"login_type": "登录类型", "login_type": "登录类型",
"pkfile": "私钥文件", "pkfile": "私钥文件",
"passphrase": "私钥密码", "passphrase": "私钥密码",
@ -157,6 +157,16 @@
"pkfile_tip": "SSH私钥文件路径", "pkfile_tip": "SSH私钥文件路径",
"passphrase_tip": "(可选)SSH私钥密码", "passphrase_tip": "(可选)SSH私钥密码",
"pkfile_selection_title": "请选择私钥文件" "pkfile_selection_title": "请选择私钥文件"
},
"sentinel": {
"title": "哨兵模式",
"enable": "当前为哨兵节点",
"master": "主节点组名",
"auto_discover": "自动查询组名",
"password": "主节点密码",
"username": "主节点用户名",
"pwd_tip": "(可选)主节点服务授权用户名",
"usr_tip": "(可选)主节点服务授权密码 (Redis > 6.0)"
} }
}, },
"group": { "group": {

View File

@ -1,5 +1,19 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { endsWith, get, isEmpty, join, remove, size, slice, sortedIndexBy, split, sumBy, toUpper, uniq } from 'lodash' import {
endsWith,
find,
get,
isEmpty,
join,
remove,
size,
slice,
sortedIndexBy,
split,
sumBy,
toUpper,
uniq,
} from 'lodash'
import { import {
AddHashField, AddHashField,
AddListItem, AddListItem,
@ -221,9 +235,34 @@ const useConnectionStore = defineStore('connections', {
pkFile: '', pkFile: '',
passphrase: '', passphrase: '',
}, },
sentinel: {
enable: false,
master: 'mymaster',
username: '',
password: '',
},
} }
}, },
mergeConnectionProfile(dest, src) {
const mergeObj = (destObj, srcObj) => {
for (const k in srcObj) {
const t = typeof srcObj[k]
if (t === 'string') {
destObj[k] = srcObj[k] || destObj[k] || ''
} else if (t === 'number') {
destObj[k] = srcObj[k] || destObj[k] || 0
} else if (t === 'object') {
mergeObj(destObj[k], srcObj[k] || {})
} else {
destObj[k] = srcObj[k]
}
}
return destObj
}
return mergeObj(dest, src)
},
/** /**
* get database server by name * get database server by name
* @param name * @param name
@ -246,6 +285,23 @@ const useConnectionStore = defineStore('connections', {
return null return null
}, },
/**
* get database by server name and index
* @param {string} connName
* @param {number} db
* @return {{}|null}
*/
getDatabase(connName, db) {
const dbs = this.databases[connName]
if (dbs != null) {
const selDB = find(dbs, (item) => item.db === db)
if (selDB != null) {
return selDB
}
}
return null
},
/** /**
* create a new connection or update current connection profile * create a new connection or update current connection profile
* @param {string} name set null if create a new connection * @param {string} name set null if create a new connection
@ -337,7 +393,7 @@ const useConnectionStore = defineStore('connections', {
label: db[i].name, label: db[i].name,
name: name, name: name,
keys: db[i].keys, keys: db[i].keys,
db: i, db: db[i].index,
type: ConnectionType.RedisDB, type: ConnectionType.RedisDB,
isLeaf: false, isLeaf: false,
children: undefined, children: undefined,
@ -465,10 +521,14 @@ const useConnectionStore = defineStore('connections', {
throw new Error(msg) throw new Error(msg)
} }
const { keys = [] } = data const { keys = [] } = data
const dbs = this.databases[connName] const selDB = this.getDatabase(connName, db)
dbs[db].opened = true if (selDB == null) {
return
}
selDB.opened = true
if (isEmpty(keys)) { if (isEmpty(keys)) {
dbs[db].children = [] selDB.children = []
return return
} }
@ -484,9 +544,12 @@ const useConnectionStore = defineStore('connections', {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async reopenDatabase(connName, db) { async reopenDatabase(connName, db) {
const dbs = this.databases[connName] const selDB = this.getDatabase(connName, db)
dbs[db].children = undefined if (selDB == null) {
dbs[db].isLeaf = false return
}
selDB.children = undefined
selDB.isLeaf = false
this._getNodeMap(connName, db).clear() this._getNodeMap(connName, db).clear()
}, },
@ -497,10 +560,13 @@ const useConnectionStore = defineStore('connections', {
* @param db * @param db
*/ */
closeDatabase(connName, db) { closeDatabase(connName, db) {
const dbs = this.databases[connName] const selDB = this.getDatabase(connName, db)
delete dbs[db].children if (selDB == null) {
dbs[db].isLeaf = false return
dbs[db].opened = false }
delete selDB.children
selDB.isLeaf = false
selDB.opened = false
this._getNodeMap(connName, db).clear() this._getNodeMap(connName, db).clear()
}, },
@ -658,12 +724,16 @@ const useConnectionStore = defineStore('connections', {
return result return result
} }
const separator = this._getSeparator(connName) const separator = this._getSeparator(connName)
const dbs = this.databases[connName] const selDB = this.getDatabase(connName, db)
if (dbs[db].children == null) { if (selDB == null) {
dbs[db].children = [] return result
}
if (selDB.children == null) {
selDB.children = []
} }
const nodeMap = this._getNodeMap(connName, db) const nodeMap = this._getNodeMap(connName, db)
const rootChildren = dbs[db].children const rootChildren = selDB.children
for (const key of keys) { for (const key of keys) {
const keyParts = split(key, separator) const keyParts = split(key, separator)
const len = size(keyParts) const len = size(keyParts)
@ -757,7 +827,7 @@ const useConnectionStore = defineStore('connections', {
*/ */
_tidyNode(connName, db, key, skipResort) { _tidyNode(connName, db, key, skipResort) {
const nodeMap = this._getNodeMap(connName, db) const nodeMap = this._getNodeMap(connName, db)
const dbNode = get(this.databases, [connName, db], {}) const dbNode = this.getDatabase(connName, db) || {}
const separator = this._getSeparator(connName) const separator = this._getSeparator(connName)
const keyParts = split(key, separator) const keyParts = split(key, separator)
const totalParts = size(keyParts) const totalParts = size(keyParts)
@ -848,7 +918,7 @@ const useConnectionStore = defineStore('connections', {
const nodeMap = this._getNodeMap(server, db) const nodeMap = this._getNodeMap(server, db)
return nodeMap.get(keyPart) return nodeMap.get(keyPart)
} else { } else {
return this.databases[server][db] return this.getDatabase(server, db)
} }
}, },
@ -1266,7 +1336,7 @@ const useConnectionStore = defineStore('connections', {
* @private * @private
*/ */
_deleteKeyNode(connName, db, key, isLayer) { _deleteKeyNode(connName, db, key, isLayer) {
const dbRoot = get(this.databases, [connName, db], {}) const dbRoot = this.getDatabase(connName, db) || {}
const separator = this._getSeparator(connName) const separator = this._getSeparator(connName)
if (dbRoot == null) { if (dbRoot == null) {

View File

@ -82,16 +82,7 @@ const useDialogStore = defineStore('dialog', {
async openEditDialog(name) { async openEditDialog(name) {
const connStore = useConnectionStore() const connStore = useConnectionStore()
const profile = await connStore.getConnectionProfile(name) const profile = await connStore.getConnectionProfile(name)
const assignCustomizer = (objVal, srcVal, key) => { this.connParam = connStore.mergeConnectionProfile(connStore.newDefaultConnection(name), profile)
if (isEmpty(objVal)) {
return srcVal
}
if (isEmpty(srcVal)) {
return objVal
}
return undefined
}
this.connParam = assignWith({}, connStore.newDefaultConnection(name), profile, assignCustomizer)
this.connType = ConnDialogType.EDIT this.connType = ConnDialogType.EDIT
this.connDialogVisible = true this.connDialogVisible = true
}, },

View File

@ -12,6 +12,7 @@ import { BrowserOpenURL } from 'wailsjs/runtime/runtime.js'
import { i18nGlobal } from '@/utils/i18n.js' import { i18nGlobal } from '@/utils/i18n.js'
import { enUS, NButton, NSpace, useOsTheme, zhCN } from 'naive-ui' import { enUS, NButton, NSpace, useOsTheme, zhCN } from 'naive-ui'
import { h, nextTick } from 'vue' import { h, nextTick } from 'vue'
import { compareVersion } from '@/utils/version.js'
const osTheme = useOsTheme() const osTheme = useOsTheme()
const usePreferencesStore = defineStore('preferences', { const usePreferencesStore = defineStore('preferences', {
@ -244,7 +245,11 @@ const usePreferencesStore = defineStore('preferences', {
const { success, data = {} } = await CheckForUpdate() const { success, data = {} } = await CheckForUpdate()
if (success) { if (success) {
const { version = 'v1.0.0', latest, page_url: pageUrl } = data const { version = 'v1.0.0', latest, page_url: pageUrl } = data
if ((manual || latest > this.general.skipVersion) && latest > version && !isEmpty(pageUrl)) { if (
(manual || latest > this.general.skipVersion) &&
compareVersion(latest, version) > 0 &&
!isEmpty(pageUrl)
) {
const notiRef = $notification.show({ const notiRef = $notification.show({
title: i18nGlobal.t('dialogue.upgrade.title'), title: i18nGlobal.t('dialogue.upgrade.title'),
content: i18nGlobal.t('dialogue.upgrade.new_version_tip', { ver: latest }), content: i18nGlobal.t('dialogue.upgrade.new_version_tip', { ver: latest }),

View File

@ -0,0 +1,36 @@
import { get, isEmpty, map, size, split, trimStart } from 'lodash'
const toVerArr = (ver) => {
const v = trimStart(ver, 'v')
let vParts = split(v, '.')
if (isEmpty(vParts)) {
vParts = ['0']
}
return map(vParts, (v) => {
let vNum = parseInt(v)
return isNaN(vNum) ? 0 : vNum
})
}
/**
* compare two version strings
* @param {string} v1
* @param {string} v2
* @return {number}
*/
export const compareVersion = (v1, v2) => {
if (v1 !== v2) {
const v1Nums = toVerArr(v1)
const v2Nums = toVerArr(v2)
const length = Math.max(size(v1Nums), size(v2Nums))
for (let i = 0; i < length; i++) {
const num1 = get(v1Nums, i, 0)
const num2 = get(v2Nums, i, 0)
if (num1 !== num2) {
return num1 > num2 ? 1 : -1
}
}
}
return 0
}

8
go.mod
View File

@ -4,10 +4,10 @@ go 1.21
require ( require (
github.com/adrg/sysfont v0.1.2 github.com/adrg/sysfont v0.1.2
github.com/redis/go-redis/v9 v9.2.0 github.com/redis/go-redis/v9 v9.2.1
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68 github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68
github.com/wailsapp/wails/v2 v2.6.0 github.com/wailsapp/wails/v2 v2.6.0
golang.org/x/crypto v0.12.0 golang.org/x/crypto v0.14.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@ -40,8 +40,8 @@ require (
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
golang.org/x/net v0.14.0 // indirect golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.11.0 // indirect golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.12.0 // indirect golang.org/x/text v0.13.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
) )

20
go.sum
View File

@ -58,8 +58,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.2.0 h1:zwMdX0A4eVzse46YN18QhuDiM4uf3JmkOB4VZrdt5uI= github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
github.com/redis/go-redis/v9 v9.2.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@ -86,8 +86,8 @@ github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhw
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.6.0 h1:EyH0zR/EO6dDiqNy8qU5spaXDfkluiq77xrkabPYD4c= github.com/wailsapp/wails/v2 v2.6.0 h1:EyH0zR/EO6dDiqNy8qU5spaXDfkluiq77xrkabPYD4c=
github.com/wailsapp/wails/v2 v2.6.0/go.mod h1:WBG9KKWuw0FKfoepBrr/vRlyTmHaMibWesK3yz6nNiM= github.com/wailsapp/wails/v2 v2.6.0/go.mod h1:WBG9KKWuw0FKfoepBrr/vRlyTmHaMibWesK3yz6nNiM=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -104,14 +104,14 @@ golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=